1. 羊了个羊游戏需求
自定义组件:
- Brand(牌): 游戏中的牌: 玉米, 刷子, 毛线…
- Layer(图层): 图层是一个有X轴和Y轴的二维矩阵关系 - 重难点之一(涉及很多二维数组和一维数组的转换和遍历)
3.Map(地图功能): 也是一个重难点(涉及到一个多图层的层级问题: 游戏中牌的遮盖问题, 被遮住的牌是灰色的 不能点的; 当上面的牌被消除掉, 下面的灰色牌变亮,可以的点击)
4.Cell- ElimiateBox(消除区域): 相同图片要按照顺序排到一起, 当满足3个之后, 三个都要消除掉
- Music(音乐)
2. 类的创建和窗口初始化
创建 cn.tedu.
model : 基本类
test: 测试类
util : 工具类
view: Start.class - 入口类, 程序的启动类
3. 创建窗口 - Start.class
// JFrame 是JDK提供的类,继承JFrame之后就可以绘制窗口, 使用窗口的的一些API
public class Start extends JFrame {
private Brand brand = new Brand("刷子");
// 无参构造器: fn + Alt + insert
public Start() throws HeadlessException {
// 设置窗口标题
this.setTitle("Java版-羊了个羊");
// 设置窗口大小
this.setSize(480,800);
// 默认关闭窗口不关闭进程 - 关闭窗口同时关闭进程
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 设置窗口居中
this.setLocationRelativeTo(null);
// 设置窗口显示
this.setVisible(true);
}
public static void main(String[] args) {
new Start();
}
}
1.Map类
游戏中最顶层的数据模型是Map, 我们叫做地图类; 包含了所有的元素, 比如:背景, 多个图层, 消除框, 道具等一些内容
2.layer
一个Map当中有多个图层Layer, 层层遮盖, 被盖住的牌就是灰色的不能点击; 这是游戏的关键点; 图层是二维表格, 每个 单元格是一个Cell类的对象
3.cell
Cell类 单元格, 一个图层当中不是所有单元格都有图案, 有的单元格是空的; 所以单元格Cell类有两种状态, 有牌和无牌状态
4.Brand
每个单元格都包含牌, Brand类, 被盖住的牌是灰色的, 不能点击的; 所以牌Brand也有两种状态, 就是灰色 和 不是灰色
总结
- 自顶向下 Map, Layer, Cell, Brand组成了整个养了个羊游戏的数据结构
- 理解一下游戏的整体数据模型: 一个地图(Map)有多个图层(Layer),一个图层(Layer)有多个单元格(Cell),一个单元格(Cell)包含0 或者1个牌(Brand), 一个牌(Brand)包含2张图片(1张正常, 一张灰色)
- 现实中可能会有关卡的类 我们模拟过程中不实现 感兴趣的同学可以在学完之后 在我们上课的基础上进行优化
Brand类: Brand 代表游戏中的一张牌
name属性 : String 类型, 存储当前牌的名称
- name属性有两个作用: 第一, 在消除框有一个基本的消除规则是 三个相同的牌就消除掉, 牌是否相同的判断依据是 name 属性; 第二, 通过name属性找到图片文件, 我们在根目录下创建 imgs 文件夹来存放游戏的素材文件
是否置灰:
正常图片
灰色图片
牌的位置: x轴 和 y轴坐标
牌的大小: width 和 height
最后生成对应的get 和 set 方法 以及构造方法: 快捷键 fn+alt+insert
// 绘制牌是需要继承 Component
// 在JFrame中 具有了添加组件的方法 -- Start.class
// 添加组件的方法
// 可以添加 自己定义的组件 到当前的窗口当中
// Start.class -- this.getContentPane().add(Component);
public class Brand extends Component{
private String name; //牌的名称
private Boolean isGray; //是否置灰
private Image image; // 正常的图片
private Image grayImage; // 灰色的图片
private Image bg; //背景图片
private Integer x; // x和 y 代表 当前牌 在渲染的时候 左上角的坐标
private Integer y;
private Integer width;
private Integer height;
// name是非常关键的属性
// name的值对应imgs下的图片名称前缀
public Brand(String name) {
this.name = name;
// Toolkit是 awt 所有实际实现的抽象超类。 Toolkit类的子类用于将各种组件绑定到特定的本机工具包实现。
// getDefaultToolkit():获取默认工具包。
// getImage()返回从指定文件获取像素数据的图像,其格式可以是GIF,JPEG或PNG。
this.image = Toolkit.getDefaultToolkit().getImage("imgs\\"+name+".png");
this.grayImage = Toolkit.getDefaultToolkit().getImage("imgs\\"+name+"_灰.png");
//背景图片
this.bg = Toolkit.getDefaultToolkit().getImage("imgs\\"+name+".jpg");
this.isGray = false;
// 图片的宽高
this.width=59;
this.height=66;
// 坐标动态生成 - 目前先固定
this.x=0;
this.y=0;
}
}
// 重写绘制自身的函数 - paint()自动生成
@Override
public void paint(Graphics g) {
if(this.isGray==true){
// 绘制灰色图片
g.drawImage(this.grayImage, this.x, this.y, null);
}else{
// 绘制正常图片
g.drawImage(this.image, this.x, this.y, null);
}
}
Start类中- 添加一张牌到窗口中
public class Start extends JFrame {
// 创建Brand对象
private Brand brand = new Brand("刷子");
// 无参构造器
public Start() throws HeadlessException {
// 设置窗口居中
this.setLocationRelativeTo(null);
// 将牌改变为灰色
//brand.setGray(true);
// 绘制游戏中的牌
this.getContentPane().add(brand);
// 设置窗口显示
this.setVisible(true);
}
}
}
修改Bug - 图片显示不出来的问题 - Start.class
this.setVisible(true); 位置会影响牌的显示
需要拖动一下才能看到 - 拖动窗口实际上触发了窗口的重新绘制
如果图片无法显示, 其实是我们已经将牌添加到窗口数据结构当中, 但是窗口没有重新绘制, 所以没有显示出来
**解决:**重新定义线程, 定义 autoRefresh方法
// 定义线程 自动刷新
// 在构造方法中 调用
private void autoRefresh(){
// 将this转为局部变量,指当前窗口
Start start = this;
//快捷键 new Thread(new Runnable(){ }) .start()
new Thread(new Runnable(){
@Override
// 重写run方法
public void run() {
// 写一个死循环, 不停的调用repaint方法 - 重新绘制
while(true){
// 当前窗口的repaint(),为了准确,将this赋值给start
start.repaint();
try {
// 每个 40ms 让线程刷新一下
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
// 然后再Start() 中调用 -- this.autoRefresh();
Brand 类实现鼠标点击事件
在实际游戏中, 鼠标点击牌, 牌就从图层当中移动到 下放的消除框当中
先实现点击事件, 让牌消失的过程
public Brand(Brand name){
// 定义事件 -- 鼠标点击事件
this.addMouseListener(new MouseAdapter() {
@Override
// MouseEvent对象, 当前点击的对象,可以获取我们想要的数据
public void mouseClicked(MouseEvent e) {
// 先进行 : 测试
System.out.println("Brand.mouseClicked");
// 默认返回 Object 需要强转为 Brand
Brand brand =(Brand) e.getSource();// 获取当前的组件
brand.getParent().remove(brand); // 通过父容器删除自己 - 一般树形结构使用
}
});
}
回顾
- 一个Map当中有多个图层Layer, 层层遮盖, 被盖住的牌就是灰色的不能点击; 这是游戏的关键点; 图层是二维表格, 每个 单元格是一个Cell类的对象
- 一个图层当中不是所有单元格都有图案, 有的单元格是空的; 所以单元格Cell类有两种状态, 有牌和无牌状态
1. 单元格类-Cell
/**
* 单元格类
* 有两种状态 0 无牌 1 有牌
*/
public class Cell {
private Integer state = 0; //单元格的状态 - 有牌或者无牌
private Brand brand; // 定义一个牌对象, 有牌的情况下必须设置一个值
// 提供对应的get 和 set 方法
}
2.图层类-Layer
/**
* 图层类
* 二维表格- 使用二维数组进行存储
*/
public class Layer {
// private Cell[][] cells = new Cell[4][5];
// 默认状态下, 并不知道图层有多少行, 多少列, 应该是随机的状态
private Cell[][] cells = null;
// 提供get和 set 方法
}
3. 图层类的重要属性
rowNum, colNum: 分别代表二维数组的 有多少行 多少列
capcity; 当前图层能最多容纳的牌数量, 最大容量 假设 6 6
size; 图层目前有多少牌 – 当牌添加的时候需要改变值; 当牌减少的时候也需要改变值
- 区别: 假一辆公交车可以做10个人, 目前只坐了2个人, 那么:size=2; capcity=10;
对应的get 和 set方法
public class Layer {
// 定义行 和 列的变量
private Integer rowNum; // 有多少行
private Integer colNum; // 有多少列
private Integer capcity; // 当前图层能最多容纳的牌数量, 最大容量 6 6
//当牌添加的时候需要改变值; 当牌减少的时候也需要改变值
private Integer size; // 图层目前有多少牌
//构造方法 -设置初始值,参数: 行号 , 列号
public Layer(Integer rowNum, Integer colNum) throws Exception {
this.rowNum = rowNum;
this.colNum = colNum;
this.capcity = this.rowNum * this.colNum;
this.cells = new Cell[this.rowNum][this.colNum];
// size无法确定值, 先设置为 0
this.size=0;
}
4 图层的数据构建 - 测试类 TestBuildLayer
// 在测试类,创建图层
public class TestBuildLayer {
public static void main(String[] args) throws Exception {
Layer layer = new Layer(6, 6);
}
}
Layer类 内部有一个二维数组来存放我们的牌,但是 刚刚我们创建的二维数组是空白的, 没有任何的牌
我们需要把牌方法二维数组中, 步骤如下:
- 第一步: 创建一个数组, 存放所有牌的名称, 每次随机从中抽取一个牌的名字, 因为每次进入地图, 牌都是随机生成的, 不是整整齐齐摆好的, 那样游戏就没有乐趣了
// 因为经常用到, 所以提前保存一下变量
public static Random random = new Random();
// 2. 创建数组, 随机取牌
public static String[] brandName={
"刷子","剪刀","叉子","奶瓶",
"干草","手套","树桩","棉花",
"毛线","水桶","火","玉米",
"白菜","草","萝卜","铃铛"
};
// 3. 定义方法 getBrandName() 每次调用都随机和获取一个牌的名称
public static String getBrandName(){
// 获取随机数 : 获取随机的一个下标, 最大值是数组的长度
int randomIndex = random.nextInt(brandName.length);
return brandName[randomIndex];
}
// 在main方法中测试
// 快捷键: getBrandName().sout
// System.out.println(getBrandName());
- 第二步: 创建Brand数组, 容量小于等于图层容量的大小; 注意: 这两有一个游戏逻辑问题: 3张牌消除,如果容量不是 3的倍数, 游戏根本无法全部消除
main(){
// 创建Brands数组: 容量 与 图层容量相同
// layer.getCapcity() 当前图层能最多容纳的牌数量
Brand[] brands = new Brand[layer.getCapcity()];
for (int i = 0; i < brands.length; i++) {
System.out.println(brands[i]); //null
// 每次循环获取一个随机的牌名称
String randomBrandName = getBrandName();
// 需要通过牌的初始化构造器创建牌的对象
Brand brand = new Brand(randomBrandName);
// 将获取到的牌放到 brands数组中
brands[i] = brand;
System.out.println(brands[i]);
// 按照现在的代码执行, 不能保证牌出现3张消除 - 优化
// 基本规则: 三张相同名称的牌 清除
// 创建牌的时候直接先 生成三种相同的牌
}
// 测试当前的brands数组
System.out.println("----------- 测试打印结果-----------");
for (int i=0;i<brands.length;i++){
System.out.println(brands[i].getName()+"-");
}
// 创建牌的时候直接先 生成3张相同的牌
// i = i+3 i变量3步走
for (int i = 0; i < brands.length; i=i+3) {
String randomBrandName = getBrandName(); // 每次循环获取一个随机的牌名称
Brand brand1 = new Brand(randomBrandName);
Brand brand2 = new Brand(randomBrandName);
Brand brand3 = new Brand(randomBrandName);
brands[i] = brand1;
brands[i+1] = brand2;
brands[i+2] = brand3;
}
// 带来问题: 如果数组的容量不是3的倍数, 会报错 --- 解决
}
解决: 1.容量必须是3的倍数, 也就是在初始化对象的时候, 就判断一下, 如果不能被3整除, 那么就不能创建图层对象, 抛出自定义的异常对象 -
Layer.java
构造方法中
// 控制容量为3的倍数
public Layer(){
if(this.capcity%3!=0){
throw new Exception("容量不是3的倍数");
}
this.cells = new Cell[this.rowNum][this.colNum];
}
- 第三步: 循环获取牌的名称, 每次产生3个相同的名字的 牌对象, 然后放入 Brand数组中, 注意: 循环的计数器也需要加3, 而不是++
- 第四步: 把牌的数组 打乱顺序 乱序
for (int i=0;i<brands.length;i++){
//先获取 当前位置 A 的变量
// A的索引brandA
Brand brandA = brands[i];
// 交换位置的 B 的随机索引位置
// 先获取一个随机的整数
int randomIndex = random.nextInt(brands.length);
// 随机交互索引B的位置 brandB
Brand brandB = brands[randomIndex];
// brandA 与 brandB进行位置交换, 先保存一下brandA的位置
// 防止将B位置赋值给A时,将A的位置丢失掉
Brand temp = brandA;
brands[i]=brandB;
brands[randomIndex]=temp;
}
System.out.println("--------打乱顺序后打印结果--------------");
for (int i=0;i<brands.length;i++){
System.out.println(brands[i].getName()+"-");
}
- 第五步: 把 牌 填充到图层当中, 并打印目前的图层
把一张一张牌, 填充到每一个单元格中, Cell是二维数组
// 将数组填充到单元格中
// 通过图层, 获取到每一个单元格 -- 返回二维数组
Cell[][] cells = layer.getCells();
// 遍历二维数组
int flag = 0;
// 先遍历行号
for(int row=0;row<cells.length;row++){
// 在遍历列号
for (int col=0;col<cells[row].length;col++){
System.out.println(row +"-"+col);
// 初始化一个单元格对象 - 两个值 state brand
Cell cell = new Cell();
cell.setState(1);
// 通过setBrand方法把牌放进来, 牌在brands一维数组里
// 可以在外部定义一个flag变量, 让其++
cell.setBrand(brands[flag++]);
// 形成cell对象后, 放大二维数组cell中 -- 非常重要
cells[row][col] = cell; //把之前空的 图层,设置了值
}
}
System.out.println("--------输出layer图层的值--------------");
for(int row=0;row<cells.length;row++){
for (int col=0;col<cells[row].length;col++){
Brand brands1 = cells[row][col].getBrand();
System.out.print(brands1.getName()+"-");
}
System.out.println();
}
整体代码
/*
测试图层数据构建完整代码
*/
public class TestBuildLayer {
// 因为经常用到, 所以提前保存一下变量
public static Random random = new Random();
// 2. 创建数组, 随机取牌
public static String[] brandName={
"刷子","剪刀","叉子","奶瓶",
"干草","手套","树桩","棉花",
"毛线","水桶","火","玉米",
"白菜","草","萝卜","铃铛"
};
// 3. 定义方法 getBrandName() 每次调用都随机和获取一个牌的名称
public static String getBrandName(){
// 获取随机的一个下标, 最大值是数组的长度
int randomIndex = random.nextInt(brandName.length);
return brandName[randomIndex];
}
public static void main(String[] args) throws Exception {
// 1. 先创建容量为 6*6 的图层
Layer layer = new Layer(6, 6);
// 创建Brands数组: 容量 与 图层容量相同
Brand[] brands = new Brand[layer.getCapcity()];
// 循环-快捷键: brands.fori
//for (int i = 0;i
// 每次循环获取一个随机的牌名称
//String randomBrandName = getBrandName();
// 需要通过牌的初始化构造器创建牌的对象
//Brand brand = new Brand(randomBrandName);
//将获取到的牌放到 brands数组中
//brands[i] = brand;
// 按照现在的代码执行, 不能保证牌出现3张消除 - 优化
// 基本规则: 三张相同名称的牌 清除
// 创建牌的时候直接先生成三种相同的牌
// i+3 带来新的问题,如果数组容量不是3的倍数, 会报错,数组越界
// 为了游戏规则, 让 容量为3的倍数
// 判断 layer类下的capcity为3的倍数
//this.capcity = this.rowNum * this.colNum;
// 控制容量为3的倍数
//if(this.capcity%3!=0){
//throw new Exception("容量不是3的倍数");
//}
for (int i = 0;i<brands.length;i+=3){
String randomBrandName = getBrandName(); // 每次循环获取一个随机的牌名称
Brand brand1 = new Brand(randomBrandName);
Brand brand2 = new Brand(randomBrandName);
Brand brand3 = new Brand(randomBrandName);
brands[i] = brand1;
brands[i+1] = brand2;
brands[i+2] = brand3;
}
System.out.println("----------- 测试打印结果-----------");
for (int i=0;i<brands.length;i++){
System.out.println(brands[i].getName()+"-");
}
//问题: 生成数组的都是顺序整齐的, 要随机打乱
System.out.println("----------随机打乱的代码------------");
for (int i=0;i<brands.length;i++){
//先获取 当前位置 A 的变量
// A的索引brandA
Brand brandA = brands[i];
// 交换位置的 B 的随机索引位置
// 先获取一个随机的整数
int randomIndex = random.nextInt(brands.length);
// 随机交互索引B的位置 brandB
Brand brandB = brands[randomIndex];
// brandA 与 brandB进行位置交换, 先保存一下brandA的位置
// 防止将B位置赋值给A时,将A的位置丢失掉
Brand temp = brandA;
brands[i]=brandB;
brands[randomIndex]=temp;
}
System.out.println("--------打乱顺序后打印结果--------------");
for (int i=0;i<brands.length;i++){
System.out.println(brands[i].getName()+"-");
}
// 把牌填充到图层当中, 并打印目前的图层
// layer.getCells 会返回二维数组
Cell[][] cells = layer.getCells();
// cells是二维数组
// brands是为数组
//将牌一个一个填充到二维数组中 - 二维数组的遍历
/**
* * * * *
* * * * *
* * * * *
*/
int flag = 0;
// 先遍历行号
for(int row=0;row<cells.length;row++){
// 在遍历列号
for (int col=0;col<cells[row].length;col++){
System.out.println(row +"-"+col);
// 初始化一个单元格对象 - 两个值 state brand
Cell cell = new Cell();
cell.setState(1);
// 通过setBrand方法把牌放进来, 牌在brands一维数组里
// 可以在外部定义一个flag变量, 让其++
cell.setBrand(brands[flag++]);
// 形成cell对象后, 放大二维数组cell中 -- 非常重要
cells[row][col] = cell; //把之前空的 图层,设置了值
}
}
System.out.println("--------输出layer图层的值--------------");
for(int row=0;row<cells.length;row++){
for (int col=0;col<cells[row].length;col++){
Brand brands1 = cells[row][col].getBrand();
System.out.print(brands1.getName()+"-");
}
System.out.println();
}
}
}
}
5.代码重构
在测试类中实现了图层数据的构建,代码都堆在一起, 后期很难阅读和维护
要进行代码的重构, 步骤如下:
- 第一: 创建BrandUtil工具类,用来封装创建随机牌的功能代码
- 第二: 创建LayerUtil工具类, 用来封装创建图层数据的功能代码
- 第三: 对外提供统一的访问入口
BrandUtil.class
/**
* 工具类
* 提供 创建牌相关的一些 公共方法
*/
public class BrandUtil {
public static Random random = new Random();
public static String[] brandName={
"刷子","剪刀","叉子","奶瓶",
"干草","手套","树桩","棉花",
"毛线","水桶","火","玉米",
"白菜","草","萝卜","铃铛"
};
//getBrandName() 每次调用都随机和获取一个牌的名称
public static String getBrandName(){
int randomIndex = random.nextInt(brandName.length);
return brandName[randomIndex];
}
// 定义对外的统一的访问入口
// 创建随机牌的数组 - 传参
public static Brand[] buildBrand(Integer capacity){
Brand[] brands = new Brand[capacity];
for (int i = 0;i<brands.length;i+=3){
String randomBrandName = getBrandName(); // 每次循环获取一个随机的牌名称
Brand brand1 = new Brand(randomBrandName);
Brand brand2 = new Brand(randomBrandName);
Brand brand3 = new Brand(randomBrandName);
brands[i] = brand1;
brands[i+1] = brand2;
brands[i+2] = brand3;
}
for (int i=0;i<brands.length;i++){
//当前位置 A的变量拿到
Brand brandA = brands[i];
// 交换位置的 B索引
int randomIndex = random.nextInt(brands.length);
Brand brandB = brands[randomIndex];
Brand temp = brandA;
brands[i]=brandB;
brands[randomIndex]=temp;
}
return brands;
}
}
LayerUtil.class
/**
* 创建图层 构建图层的方法
*/
public class LayerUtil {
public static Layer build(Integer rowNum, Integer colNum){
Layer layer = null; // 容量为36的二维数组图层
try {
layer = new Layer(rowNum, colNum);
} catch (Exception e) {
e.printStackTrace();
}
//System.out.println(getBrandName());
// 目前的代码 可以生成,但是不保证是至少3个
Brand[] brands = BrandUtil.buildBrand(layer.getCapcity());
Cell[][] cells = layer.getCells();
int flag = 0;
for(int row=0;row<cells.length;row++){
for (int col=0;col<cells[row].length;col++){
System.out.println(row +"-"+col);
Brand brands1 = brands[flag++];
Cell cell = new Cell();
cell.setState(1);
// 将牌 给到单元格对象
cell.setBrand(brands1);
// 让牌 反向找到单元格对象
brands1.setCell(cell);
cells[row][col] = cell; //把2之前空的 图层,设置了值
}
}
//System.out.println("-----调用showCells方法查看当前图层的数据------");
// layer.showCells();
return layer;
}
}
Layer.java - 显示图层
//定义 showCells方法, 展示目前图层的数据
public void showCells(){
for(int row=0;row<cells.length;row++){
for (int col=0;col<cells[row].length;col++){
Brand brands1 = cells[row][col].getBrand();
System.out.print(brands1.getName()+"-");
}
System.out.println();
}
}
6. 测试渲染图层的功能-TestRenderLayer.java
/**
* 测试渲染一个图层的数据
*/
public class TestRenderLayer extends JFrame{
// 1. 通过LayUtil.build 方法, 获取图层对象
private Layer layer = LayerUtil.build(6, 6);
// 2. 定义构造器
public TestRenderLayer(){
// 1)作用: 初始化窗口的基本信息
init();
// 2)渲染图层
renderLayer();
// 3)自动刷新
autoRefresh();
}
// 3. 初始化窗口的基本信息的方法
private void init(){
this.setTitle("Java版-羊了个羊");
this.setSize(800,1200);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLocationRelativeTo(null);
//设置绝对布局
this.setLayout(null);
this.setBounds(0,0,480,800);
// 设置窗口显示
this.setVisible(true);
}
// 4. 定义自动刷新代码
private void autoRefresh(){
JFrame start = this;
new Thread(new Runnable(){
@Override
public void run() {
while(true){
start.repaint();
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
//5. 渲染图层的方法 this.getContentPane().add(Compontent);
// 有36个单元格,需要一个一个遍历
// 先获取到 图层的二维数组
// 将二维数组进行循环遍历 - 复制粘贴代码
private void renderLayer(){
Cell[][] cells = layer.getCells();
for(int row=0;row<cells.length;row++){
for (int col=0;col<cells[row].length;col++){
Brand brands1 = cells[row][col].getBrand();
// 以上步骤走完, 所有 牌显示在一个位置 :
// 默认情况下, brand 牌的左上角坐标是0 0 所以 我们要改变 牌的坐标
// 需要修改坐标
// 第二 布局方式 默认 swing窗口添加组件 提供了多种布局方式
// 如: 网格布局, 流式布局 以及 绝对布局
// 常用: 绝对布局
// 在init方法中:
// this.setLayout(null)
// this.setBounds(0,0,480,800);
int x = col * 59; // 列号*59 0 59 118 177
int y = row * 66; // 行号*66
brands1.setBounds(x,y,59,66);
this.getContentPane().add(brands1);
}
System.out.println();
}
}
// main方法
public static void main(String[] args) {
new TestRenderLayer();
}
}