-
Notifications
You must be signed in to change notification settings - Fork 0
Home
LiuJiawei4132 edited this page Mar 30, 2019
·
5 revisions
用了几天的时间,借鉴了下别人的代码做出来的2048,虽然大部分是借鉴人家的代码,但是写出来的时候成就感还是很强的, 并且也第一次接触到了MVC程序设计模式,我个人认为大概的方式就是:先设计好视图,再建立起基本的模型, 最后的再加入控制器调试。最关键的内容还是瓦片的合并算法以及类的设计。
效果如下:
- javax.swing.
- JFrame 窗口
- JLable 标签
- JPanel 面板
- java.awt.event.
- KeyEvent 键盘监听事件号
- KeyListener 键盘监听
- java.util.
- ArrayList 根据下标遍历、访问元素效率较高,但增删效率较慢
- List
- Random 获取随机数
- 主函数
public class GameMain {
public static void main(String[] args) {
Window window = new Window();
window.initView();
window.setTitle("D2048"); // 设置标题
window.getContentPane().setPreferredSize(new Dimension(400, 500)); //对JFrame添加组件的一种方式
//JFrame直接调用setBackground设置背景色不生效
window.getContentPane().setBackground(new Color(0xfaf8ef)); // 设置背景颜色
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 退出程序
window.setResizable(false); //去掉最大化按钮
window.pack(); //获得最佳大小
window.setVisible(true); // 设置可见
}
}
- 接下来要思考需要用到的变量
class Window extends JFrame {
private static int score = 0; // 分数
final Font[] fonts = {new Font("", Font.BOLD, 48)
, new Font("", Font.BOLD, 42)
, new Font("", Font.BOLD, 36)
, new Font("", Font.BOLD, 30)
, new Font("", Font.BOLD, 24)
};
private GameBoard gameBoard;
private JLabel ltitle;
private JLabel lsctip;
private JLabel lgatip;
private JLabel lscore;
}
- 配置并添加组件至Windows面板
public void initView() {
ltitle = new JLabel("2048", JLabel.CENTER); // 建立一个JLabel标签
ltitle.setFont(new Font("", Font.BOLD, 50)); // 设置字体
ltitle.setForeground(new Color(0x776e65)); // 设置前景颜色
ltitle.setBounds(0, 0, 120, 60); // 设置大小和位置
lgatip = new JLabel("按方向键可以控制方块的移动,按ESC键可以重新开始游戏.", JLabel.CENTER);
lgatip.setFont(new Font("", Font.ITALIC, 13));
lgatip.setForeground(new Color(0x776e65));
lgatip.setBounds(10, 75, 340, 15);
lsctip = new JLabel("SCORE", JLabel.CENTER); // 建立一个JLabel标签
lsctip.setFont(new Font("", Font.BOLD, 18)); // 设置字体
lsctip.setForeground(new Color(0xeee4da)); // 设置前景颜色
lsctip.setOpaque(true); // 设置透明度
lsctip.setBackground(new Color(0xbbada0)); // 设置背景颜色
lsctip.setBounds(290, 5, 100, 25); // 设置背景大小以及位置
lscore = new JLabel("0", JLabel.CENTER);
lscore.setFont(new Font("", Font.BOLD, 25));
lscore.setForeground(Color.WHITE);
lscore.setOpaque(true);
lscore.setBackground(new Color(0xbbada0));
lscore.setBounds(290, 30, 100, 25);
gameBoard = new GameBoard(); //
gameBoard.setPreferredSize(new Dimension(400, 400)); // 设置一个大小动态的窗口
gameBoard.setBackground(new Color(0xbbada0));
gameBoard.setBounds(0, 100, 400, 400); // 主要游戏面板为400×400
gameBoard.setFocusable(true); // 设置控件为可获取焦点状态,只能设置成true才能获取事件
this.add(ltitle);
this.add(lsctip);
this.add(lgatip);
this.add(lscore);
this.add(gameBoard);
}
效果如下:
- 接下来就要考虑Tile的类该怎么写了 先想好Tile所具有的属性,例如每个Tile的数值、Tile是否已经合成了、Tile该如何初始化?
class Tile {
public int value; // 记录瓦片的值
public boolean isMerge; // 判断是否为合成瓦片
public Tile() {
clearData();
}
public void clearData() { // 初始化瓦片
this.value = 0;
this.isMerge = false;
}
}
考虑到每个瓦片对应的数值不同,字体大小以及背景颜色都不同,所以我们要给写个获取颜色和大小的方法
public Font getFont() { // 获取字体大小
int index = value < 100 ? 1 : value < 1000 ? 2 : value < 10000 ? 3 : 4;
return fonts[index];
}
public Color getForeground() { // 设置字体
switch (value) {
case 0:
// 颜色与瓦块背景色相同,因为每个瓦片初始值都为0,所以这时的字体颜色应与背景颜色一样
return new Color(0xcdc1b4);
case 2:
case 4:
return new Color(0x776e65);
default:
return new Color(0xf9f6f2);
}
}
public Color getBackground() {
switch (value) {
case 0:
return new Color(0xcdc1b4);
case 2:
return new Color(0xeee4da);
case 4:
return new Color(0xede0c8);
case 8:
return new Color(0xf2b179);
case 16:
return new Color(0xf59563);
case 32:
return new Color(0xf67c5f);
case 64:
return new Color(0xf65e3b);
case 128:
return new Color(0xedcf72);
case 256:
return new Color(0xedcc61);
case 512:
return new Color(0xedc850);
case 1024:
return new Color(0xedc53f);
case 2048:
return new Color(0xedc22e);
case 4096:
return new Color(0x65da92);
case 8192:
return new Color(0x5abc65);
case 16384:
return new Color(0x248c51);
default:
return new Color(0x248c51);
}
}
- 接下来我们就要给瓦片画个背景啦
class GameBoard extends JPanel {
private static final int GAP_TILE = 16; // 4×80 = 320 间隙大小为 (400 - 320)/5 = 16
private static final int ARC_TILE = 16; //瓦片圆角弧度
private static final int SIZE_TILE = 80;//瓦片的大小
@Override
public void paint(Graphics g) {
super.paint(g); // 覆写paint方法一定要先执行super.paint(g);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
drawTile(g, i, j); 绘制4×4瓦片
}
}
}
public void drawTile(Graphics gg, int i, int j) {
Graphics2D g = (Graphics2D)gg; // 向下转型成更强的画笔
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON); // 为画笔加上抗锯齿特效
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_NORMALIZE); // 笔划规范化控制提示值——几何形状应当规范化
Tile tile = tiles[i][j];
// 瓦片背景色
g.setColor(tile.getBackground());
// 绘制圆角矩形
//fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight);
g.fillRoundRect(GAP_TILE + (GAP_TILE + SIZE_TILE) * j // 位置 = 间距 + (瓦片大小 + 间距 * 位置)
, GAP_TILE + (GAP_TILE + SIZE_TILE) * i
, SIZE_TILE, SIZE_TILE, ARC_TILE, ARC_TILE);
// 字体颜色
g.setColor(tile.getForeground());
Font font = tile.getFont();
g.setFont(font);
FontMetrics fms = getFontMetrics(font);
String value = String.valueOf(tile.value); // 将int转化成String
g.drawString(value, GAP_TILE + (GAP_TILE + SIZE_TILE) * j
+ (SIZE_TILE - fms.stringWidth(value)) / 2 // 数值横坐标位置 = 瓦片位置 + (瓦片大小 + 字体长度) / 2
,GAP_TILE + (GAP_TILE + SIZE_TILE) * i
+ (SIZE_TILE - fms.getAscent() - fms.getDescent()) / 2
+ fms.getAscent());
}
}
效果如下
那么2048游戏的界面就画好啦,接下来就需要根据游戏规则实现功能啦
- 随机生成两个瓦片
- 瓦片数值为 2 or 4
class GameBoard extends JPanel {
private Tile[][] tiles = new Tile[4][4];
private boolean isEnd;
private boolean isMove;
public void createTile() { // 生成一个瓦片
List<Tile> tileList = getBlankTile(); // 获取数值为0的瓦片列表
if (!tileList.isEmpty()) { // 如果不为空
Random random = new Random();
int index = random.nextInt(tileList.size()); // 随机选取列表中的一个瓦片
tileList.get(index).value = random.nextInt(100) > 50 ? 2 : 4; // 为其赋值(2 or 4)
}
}
public List<Tile> getBlankTile() { // 获取空瓦片
List<Tile> list = new ArrayList<Tile>();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (tiles[i][j].value == 0)
list.add(tiles[i][j]);
}
}
return list;
}
public GameBoard() {
initGame();
}
public void initGame() {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
tiles[i][j] = new Tile();
}
}
score = 0; // 初始化分数
createTile(); // 随机生成瓦片
createTile();
lscore.setText("0"); // 分数为0
isMove = false; // 初始化游戏状态
isEnd = false;
}
}
效果如下
- 实现瓦片移动、合并、生成
class GameBoard extends JPanel implements KeyListener { // 在此基础上添加监听类
public GameBoard() {
initGame();
addKeyListener(this); // 初始化添加键盘监听事件
}
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_ESCAPE: // Esc按键初始化
initGame();
break;
case KeyEvent.VK_UP: // 方向键up
moveUp(); // 移动方法
movedCreateTile(); // 移动后生成瓦片
isGameOver(); // 判断游戏是否结束
break;
// 这里只举例 up键
}
repaint();
}
public boolean moveUp() {
for (int i = 0; i < 4; i++) {
for (int j = 1; j < 4; j++) {
// 判断当前瓦片数值不能为空,前一个瓦片不能是合并瓦片,不能是瓦片的边界
for (int k = j; k > 0 && tiles[k][i].value != 0 && !tiles[k-1][i].isMerge; k--) {
if (tiles[k-1][i].value == 0) {
doMove(tiles[k-1][i], tiles[k][i]);
} else if (tiles[k-1][i].value == tiles[k][i].value) {
doMerge(tiles[k-1][i], tiles[k][i]);
break;
}
}
}
}
return isMove;
}
public void doMove(Tile fir, Tile sec) {
fir.swap(sec);
sec.clearData();
isMove = true;
}
public void doMerge(Tile fir, Tile sec) {
fir.value = fir.value << 1;
fir.isMerge = true;
sec.clearData();
score += fir.value;
isMove = true;
}
public void movedCreateTile() { // 生成瓦片
if (isMove) {
createTile();
· isMove = false;
}
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
}
- 判断游戏是否结束并打印提示语
public void isGameOver() {
lscore.setText("" + score);·
if (!getBlankTile().isEmpty()) {
return;
}
for (int i = 0; i < 4; i++) {
for (int j = 3; j > 0; j--) {
if (tiles[j-1][i].value == tiles[j][i].value || tiles[i][j-1].value == tiles[i][j].value)
return;
}
}
isEnd = true;
}
@Override
public void paint(Graphics g) { // 添加
// 在paint方法原有上添加功能
if (isEnd) {
g.setColor(new Color(255,255,255,180));
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(new Color(0x3d79ca));
g.setFont(fonts[0]);
FontMetrics fms = getFontMetrics(fonts[0]);
String value = "Game over";
String score = "SCORE:" + Window.score;
g.drawString(value, (getWidth() - fms.stringWidth(value)) / 2 , getHeight() / 3);
g.drawString(score + "", (getWidth() - fms.stringWidth(score)) / 2, getHeight() / 2 + getHeight() / 4);
}
}
效果如下
2048小游戏的代码虽然不多,但是要做到分配好功能和类也还是一件不容易的事情。每个类要有他特有的功能,这个类需要完成什么功能,哪些功能可以做成一个方法。例如getBlankTile()方法,返回数值为空的瓦片列表。我以前的想法一定是将他和createTile()方法合并起来,并不会考虑到哪里还会再次用到这个方法。
还有的就是列表的使用,在看完列表的功能后我也并没有想过到底又哪些运用到它的地方,那么生成随机瓦片的画风一定是随机在二维数组中抽一个瓦片,判断它数值是否为0,如果不是就继续判断下去,直到没有空的瓦片,现在想想我还是真的蠢哈哈哈哈。
而这里它的做法是,创建一个容器,将所有空的瓦片添加到列表中,要生成的瓦片就在这个容器里面抓,真是太棒了。
类似的功能还有很多很多,这些都是我写代码时不曾注意到的地方。
public void createTile() {
List<Tile> tileList = getBlankTile();
if (!tileList.isEmpty()) {
Random random = new Random();
int index = random.nextInt(tileList.size());
tileList.get(index).value = random.nextInt(100) > 50 ? 2 : 4;
}
}
public List<Tile> getBlankTile() {
List<Tile> list = new ArrayList<Tile>();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (tiles[i][j].value == 0)
list.add(tiles[i][j]);
}
}
return list;
}
public void isGameOver() {
lscore.setText("" + score);
if (!getBlankTile().isEmpty()) {
return;
}
for (int i = 0; i < 4; i++) {
for (int j = 3; j > 0; j--) {
if (tiles[j-1][i].value == tiles[j][i].value || tiles[i][j-1].value == tiles[i][j].value)
return;
}
}
isEnd = true;
}