├── README.md
└── src
├── com
└── game
│ ├── main
│ ├── DirectEnum.java
│ ├── GameMain1.java
│ ├── GameMain2.java
│ └── WaveThread.java
│ └── view
│ ├── Window1.java
│ └── Window2.java
└── resources
├── merge.wav
└── move.wav
/README.md:
--------------------------------------------------------------------------------
1 | # G2048 -- Java版本的2048游戏
2 |
3 | 实现的效果图如下所示:
4 |
5 | 
6 |
7 |
8 | 游戏的实现过程可以参考我网站的两篇文章:
9 |
10 | [《2048》游戏Java版本+源码(一)](http://www.wolfbe.com/detail/201609/366.html)
11 |
12 | [《2048》游戏Java版本+源码(二)](http://www.wolfbe.com/detail/201609/369.html)
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/com/game/main/DirectEnum.java:
--------------------------------------------------------------------------------
1 | package com.game.main;
2 |
3 | /**
4 | * @author laochunyu
5 | * @version 1.0
6 | * @date 2016/9/23
7 | */
8 | public enum DirectEnum {
9 | NONE,
10 | LEFT,
11 | RIGHT,
12 | UP,
13 | DOWN;
14 | }
15 |
--------------------------------------------------------------------------------
/src/com/game/main/GameMain1.java:
--------------------------------------------------------------------------------
1 | package com.game.main;
2 |
3 | import com.game.view.Window1;
4 | import com.game.view.Window2;
5 |
6 | import javax.swing.*;
7 | import java.awt.*;
8 |
9 | /**
10 | * @author 锋宇
11 | * @contact QQ群399643539
12 | * @website http://www.wolfbe.com
13 | * @copyright 版权归朗度云所有
14 | */
15 | public class GameMain1 {
16 | public static void main(String[] args) {
17 | Window1 win = new Window1();
18 | win.initView();
19 | win.setTitle("2048[©版权朗度云所有]");
20 | win.getContentPane().setPreferredSize(new Dimension(400, 500));
21 | //JFrame直接调用setBackground设置背景色不生效
22 | win.getContentPane().setBackground(new Color(0xfaf8ef));
23 | win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
24 | win.setResizable(false); //去掉最大化按钮
25 | win.pack(); //获得最佳大小
26 | win.setVisible(true);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/com/game/main/GameMain2.java:
--------------------------------------------------------------------------------
1 | package com.game.main;
2 |
3 | import com.game.view.Window2;
4 |
5 | import javax.swing.*;
6 | import java.awt.*;
7 |
8 | /**
9 | * 这个版本加入了音效、移动特效、合并特效
10 | *
11 | * @author 锋宇
12 | * @contact QQ群399643539
13 | * @website http://www.wolfbe.com
14 | * @copyright 版权归朗度云所有
15 | */
16 | public class GameMain2 {
17 | public static void main(String[] args) {
18 | Window2 win = new Window2();
19 | win.initView();
20 | win.setTitle("2048[©版权朗度云所有]");
21 | win.getContentPane().setPreferredSize(new Dimension(400, 500));
22 | //JFrame直接调用setBackground设置背景色不生效
23 | win.getContentPane().setBackground(new Color(0xfaf8ef));
24 | win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
25 | win.setResizable(false); //去掉最大化按钮
26 | win.pack(); //获得最佳大小
27 | win.setVisible(true);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/com/game/main/WaveThread.java:
--------------------------------------------------------------------------------
1 | package com.game.main;
2 |
3 | import javax.sound.sampled.*;
4 | import java.io.File;
5 | import java.net.URISyntaxException;
6 | import java.net.URL;
7 |
8 | /**
9 | * 用于播放音效的线程
10 | *
11 | * @author laochunyu
12 | * @version 1.0
13 | * @date 2016/9/23
14 | */
15 | public class WaveThread extends Thread {
16 | private String filename;
17 | private final int EXTERNAL_BUFFER_SIZE = 524288; // 128Kb
18 |
19 | public WaveThread(String wavfile) {
20 | filename = wavfile;
21 | }
22 |
23 | public void run() {
24 | AudioInputStream audioInputStream = null;
25 | SourceDataLine auline = null;
26 | URL url = Thread.currentThread().getContextClassLoader().getResource(filename);
27 | try {
28 | File soundFile = new File(url.toURI());
29 | audioInputStream = AudioSystem.getAudioInputStream(soundFile);
30 | AudioFormat format = audioInputStream.getFormat();
31 | DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
32 | auline = (SourceDataLine) AudioSystem.getLine(info);
33 | auline.open(format);
34 | auline.start();
35 |
36 | int nBytesRead = 0;
37 | byte[] abData = new byte[EXTERNAL_BUFFER_SIZE];
38 | while (nBytesRead != -1) {
39 | nBytesRead = audioInputStream.read(abData, 0, abData.length);
40 | if (nBytesRead >= 0)
41 | auline.write(abData, 0, nBytesRead);
42 | }
43 | } catch (Exception e) {
44 | e.printStackTrace();
45 | } finally {
46 | if (auline != null) {
47 | auline.drain();
48 | auline.close();
49 | }
50 | }
51 | }
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/src/com/game/view/Window1.java:
--------------------------------------------------------------------------------
1 | package com.game.view;
2 |
3 | import javax.swing.*;
4 | import java.awt.*;
5 | import java.awt.event.KeyEvent;
6 | import java.awt.event.KeyListener;
7 | import java.util.ArrayList;
8 | import java.util.List;
9 | import java.util.Random;
10 |
11 | /**
12 | * 这个版本没有音效和移动特效
13 | *
14 | * @author 锋宇
15 | * @contact QQ群399643539
16 | * @website http://www.wolfbe.com
17 | * @copyright 版权归朗度云所有
18 | */
19 | public class Window1 extends JFrame {
20 |
21 | private static int score = 0; //分数
22 | final Font[] fonts = {new Font("Helvetica Neue", Font.BOLD, 48)
23 | , new Font("Helvetica Neue", Font.BOLD, 42)
24 | , new Font("Helvetica Neue", Font.BOLD, 36)
25 | , new Font("Helvetica Neue", Font.BOLD, 30)
26 | , new Font("Helvetica Neue", Font.BOLD, 24)
27 | };
28 |
29 | private GameBoard gameBoard;
30 | private JLabel ltitle;
31 | private JLabel lsctip;
32 | private JLabel lscore;
33 | private JLabel lgatip;
34 |
35 | public Window1() {
36 | this.setLayout(null);
37 | }
38 |
39 | public void initView() {
40 | ltitle = new JLabel("2048", JLabel.CENTER);
41 | ltitle.setFont(new Font("", Font.BOLD, 50));
42 | ltitle.setForeground(new Color(0x776e65));
43 | ltitle.setBounds(0, 0, 120, 60);
44 |
45 | lsctip = new JLabel("SCORE", JLabel.CENTER);
46 | lsctip.setFont(new Font("", Font.BOLD, 16));
47 | lsctip.setForeground(new Color(0xeee4da));
48 | lsctip.setOpaque(true);//只有设置为true背景色才生效
49 | lsctip.setBackground(new Color(0xbbada0));
50 | lsctip.setBounds(290, 5, 100, 25);
51 |
52 | lscore = new JLabel("0", JLabel.CENTER);
53 | lscore.setFont(new Font("Helvetica Neue", Font.BOLD, 22));
54 | lscore.setForeground(Color.WHITE);
55 | lscore.setOpaque(true);
56 | lscore.setBackground(new Color(0xbbada0));
57 | lscore.setBounds(290, 30, 100, 25);
58 |
59 | lgatip = new JLabel("按方向键可以控制方块的移动,按ESC键可以重新开始游戏。", JLabel.LEFT);
60 | lgatip.setFont(new Font("Helvetica Neue", Font.ITALIC, 13));
61 | lgatip.setForeground(new Color(0x776e65));
62 | lgatip.setBounds(10, 60, 390, 30);
63 | //游戏面板组件
64 | gameBoard = new GameBoard();
65 | gameBoard.setPreferredSize(new Dimension(400, 400));
66 | gameBoard.setBackground(new Color(0xbbada0));
67 | gameBoard.setBounds(0, 100, 400, 400);
68 | gameBoard.setFocusable(true);
69 | //把组件加入窗体
70 | this.add(ltitle);
71 | this.add(lsctip);
72 | this.add(lscore);
73 | this.add(lgatip);
74 | this.add(gameBoard);
75 | }
76 |
77 | class GameBoard extends JPanel implements KeyListener {
78 | private static final int GAP_TILE = 16; //瓦片之间间隙
79 | private static final int ARC_TILE = 16; //瓦片圆角弧度
80 | private static final int SIZE_TILE = 80;//瓦片的大小
81 |
82 | private Tile[][] tiles = new Tile[4][4];
83 | private boolean isOver;
84 | private boolean isMove;
85 |
86 | public GameBoard() {
87 | initGame();
88 | addKeyListener(this);
89 | }
90 |
91 | @Override
92 | public void keyPressed(KeyEvent e) {
93 | boolean moved = false;
94 | switch (e.getKeyCode()) {
95 | case KeyEvent.VK_ESCAPE:
96 | initGame();
97 | break;
98 | case KeyEvent.VK_LEFT:
99 | moved = moveLeft();
100 | inovkeCreateTile();
101 | checkGameOver(moved);
102 | break;
103 | case KeyEvent.VK_RIGHT:
104 | moved = moveRight();
105 | inovkeCreateTile();
106 | checkGameOver(moved);
107 | break;
108 | case KeyEvent.VK_UP:
109 | moved = moveUp();
110 | inovkeCreateTile();
111 | checkGameOver(moved);
112 | break;
113 | case KeyEvent.VK_DOWN:
114 | moved = moveDown();
115 | inovkeCreateTile();
116 | checkGameOver(moved);
117 | break;
118 | }
119 | repaint();
120 | }
121 |
122 | private void initGame() {
123 | //初始化地图
124 | for (int i = 0; i < 4; i++) {
125 | for (int j = 0; j < 4; j++) {
126 | tiles[i][j] = new Tile();
127 | }
128 | }
129 | //生成两个瓦片
130 | createTile();
131 | createTile();
132 |
133 | isMove = false;
134 | isOver = false;
135 | }
136 |
137 | private void createTile() {
138 | //获取当前空白的瓦片,并加入列表
139 | List list = getBlankTiles();
140 | if (!list.isEmpty()) {
141 | Random random = new Random();
142 | int index = random.nextInt(list.size());
143 | Tile tile = list.get(index);
144 | //初始化新瓦片的值为2或4
145 | tile.value = random.nextInt(100) > 50 ? 4 : 2;
146 | }
147 | }
148 |
149 | /**
150 | * 获取当前空白的瓦片,加入列表返回
151 | *
152 | * @return
153 | */
154 | private List getBlankTiles() {
155 | List list = new ArrayList();
156 | for (int i = 0; i < 4; i++) {
157 | for (int j = 0; j < 4; j++) {
158 | if (tiles[i][j].value == 0) {
159 | list.add(tiles[i][j]);
160 | }
161 | }
162 | }
163 | return list;
164 | }
165 |
166 | private void inovkeCreateTile(){
167 | if(isMove){
168 | createTile();
169 | isMove = false;
170 | }
171 | }
172 |
173 | private void checkGameOver(boolean moved) {
174 | lscore.setText(score + "");
175 | if (!getBlankTiles().isEmpty()) {
176 | return;
177 | }
178 | for (int i = 0; i < 3; i++) {
179 | for (int j = 0; j < 3; j++) {
180 | //判断是否存在可合并的两个瓦片
181 | if (tiles[i][j].value == tiles[i][j + 1].value || tiles[i][j].value == tiles[i + 1][j].value) {
182 | isOver = false;
183 | return;
184 | }
185 | }
186 | }
187 | isOver = true;
188 | }
189 |
190 | private boolean moveLeft() {
191 | isMove = false;
192 | for (int i = 0; i < 4; i++) {
193 | for (int j = 1; j < 4; j++) {
194 | int k = j;
195 | //当前移动瓦片不能到达边界,不能为空白瓦片,前方瓦片不能是合成瓦片
196 | while (k > 0 && tiles[i][k].value != 0 && !tiles[i][k - 1].ismerge) {
197 | if (tiles[i][k - 1].value == 0) {
198 | doMove(tiles[i][k], tiles[i][k - 1]);
199 | } else if (tiles[i][k - 1].value == tiles[i][k].value) {
200 | doMerge(tiles[i][k], tiles[i][k - 1]);
201 | break;
202 | } else {
203 | break;
204 | }
205 | k--;
206 | }
207 | }
208 | }
209 | return isMove;
210 | }
211 |
212 | private boolean moveRight() {
213 | isMove = false;
214 | for (int i = 0; i < 4; i++) {
215 | for (int j = 2; j > -1; j--) {
216 | int k = j;
217 | //当前移动瓦片不能到达边界,不能为空白瓦片,前方瓦片不能是合成瓦片
218 | while (k < 3 && tiles[i][k].value != 0 && !tiles[i][k + 1].ismerge) {
219 | if (tiles[i][k + 1].value == 0) {
220 | doMove(tiles[i][k], tiles[i][k + 1]);
221 | } else if (tiles[i][k + 1].value == tiles[i][k].value) {
222 | doMerge(tiles[i][k], tiles[i][k + 1]);
223 | break;
224 | } else {
225 | break;
226 | }
227 | k++;
228 | }
229 | }
230 | }
231 | return isMove;
232 | }
233 |
234 | private boolean moveUp() {
235 | isMove = false;
236 | for (int j = 0; j < 4; j++) {
237 | for (int i = 1; i < 4; i++) {
238 | int k = i;
239 | //当前移动瓦片不能到达边界,不能为空白瓦片,前方瓦片不能是合成瓦片
240 | while (k > 0 && tiles[k][j].value != 0 && !tiles[k - 1][j].ismerge) {
241 | if (tiles[k - 1][j].value == 0) {
242 | doMove(tiles[k][j], tiles[k - 1][j]);
243 | } else if (tiles[k - 1][j].value == tiles[k][j].value) {
244 | doMerge(tiles[k][j], tiles[k - 1][j]);
245 | break;
246 | } else {
247 | break;
248 | }
249 | k--;
250 | }
251 | }
252 | }
253 | return isMove;
254 | }
255 |
256 | private boolean moveDown() {
257 | isMove = false;
258 | for (int j = 0; j < 4; j++) {
259 | for (int i = 2; i > -1; i--) {
260 | int k = i;
261 | //当前移动瓦片不能到达边界,不能为空白瓦片,前方瓦片不能是合成瓦片
262 | while (k < 3 && tiles[k][j].value != 0 && !tiles[k + 1][j].ismerge) {
263 | if (tiles[k + 1][j].value == 0) {
264 | doMove(tiles[k][j], tiles[k + 1][j]);
265 | } else if (tiles[k + 1][j].value == tiles[k][j].value) {
266 | doMerge(tiles[k][j], tiles[k + 1][j]);
267 | break;
268 | } else {
269 | break;
270 | }
271 | k++;
272 | }
273 | }
274 | }
275 | return isMove;
276 | }
277 |
278 | private void doMove(Tile src, Tile dst) {
279 | dst.swap(src);
280 | src.clear();
281 | isMove = true;
282 | }
283 |
284 | private void doMerge(Tile src, Tile dst) {
285 | dst.value = dst.value << 1;
286 | dst.ismerge = true;
287 | src.clear();
288 | score += dst.value;
289 | isMove = true;
290 | }
291 |
292 | @Override
293 | public void paint(Graphics g) {
294 | super.paint(g);
295 | for (int i = 0; i < 4; i++) {
296 | for (int j = 0; j < 4; j++) {
297 | drawTile(g, i, j);
298 | }
299 | }
300 | if (isOver) {
301 | g.setColor(new Color(255, 255, 255, 180));
302 | g.fillRect(0, 0, getWidth(), getHeight());
303 | g.setColor(new Color(0x3d79ca));
304 | g.setFont(fonts[0]);
305 | FontMetrics fms = getFontMetrics(fonts[0]);
306 | String value = "Game Over";
307 | g.drawString(value, (getWidth() - fms.stringWidth(value)) / 2, getHeight() / 2);
308 | }
309 |
310 | }
311 |
312 | private void drawTile(Graphics gg, int i, int j) {
313 | Graphics2D g = (Graphics2D) gg;
314 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
315 | RenderingHints.VALUE_ANTIALIAS_ON);
316 | g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
317 | RenderingHints.VALUE_STROKE_NORMALIZE);
318 | Tile tile = tiles[i][j];
319 | //绘制瓦片背景
320 | g.setColor(tile.getBackground());
321 | //注意:横坐标用j计算,纵坐标用i计算
322 | g.fillRoundRect(GAP_TILE + (GAP_TILE + SIZE_TILE) * j ,
323 | GAP_TILE + (GAP_TILE + SIZE_TILE) * i ,
324 | SIZE_TILE , SIZE_TILE , ARC_TILE, ARC_TILE);
325 | //绘制瓦片文字
326 | g.setColor(tile.getForeground());
327 | Font font = tile.getTileFont();
328 | g.setFont(font);
329 | FontMetrics fms = getFontMetrics(font);
330 | String value = String.valueOf(tile.value);
331 | //注意:横坐标用j计算,纵坐标用i计算
332 | g.drawString(value, GAP_TILE + (GAP_TILE + SIZE_TILE) * j
333 | + (SIZE_TILE - fms.stringWidth(value)) / 2
334 | , GAP_TILE + (GAP_TILE + SIZE_TILE) * i
335 | + (SIZE_TILE - fms.getAscent() - fms.getDescent()) / 2
336 | + fms.getAscent());
337 | tiles[i][j].ismerge = false;
338 | }
339 |
340 | @Override
341 | public void keyTyped(KeyEvent e) {
342 |
343 | }
344 |
345 | @Override
346 | public void keyReleased(KeyEvent e) {
347 |
348 | }
349 | }
350 |
351 | class Tile {
352 | public int value;//显示的数字
353 | public boolean ismerge;//是否是合并的
354 |
355 | public Tile() {
356 | clear();
357 | }
358 |
359 | public void clear() {
360 | value = 0;
361 | ismerge = false;
362 | }
363 |
364 | public void swap(Tile tile) {
365 | this.value = tile.value;
366 | this.ismerge = tile.ismerge;
367 | }
368 |
369 | public Color getForeground() {
370 | switch (value) {
371 | case 0:
372 | return new Color(0xcdc1b4);
373 | case 2:
374 | case 4:
375 | return new Color(0x776e65);
376 | default:
377 | return new Color(0xf9f6f2);
378 | }
379 | }
380 |
381 | public Color getBackground() {
382 | switch (value) {
383 | case 0:
384 | return new Color(0xcdc1b4);
385 | case 2:
386 | return new Color(0xeee4da);
387 | case 4:
388 | return new Color(0xede0c8);
389 | case 8:
390 | return new Color(0xf2b179);
391 | case 16:
392 | return new Color(0xf59563);
393 | case 32:
394 | return new Color(0xf67c5f);
395 | case 64:
396 | return new Color(0xf65e3b);
397 | case 128:
398 | return new Color(0xedcf72);
399 | case 256:
400 | return new Color(0xedcc61);
401 | case 512:
402 | return new Color(0xedc850);
403 | case 1024:
404 | return new Color(0xedc53f);
405 | case 2048:
406 | return new Color(0xedc22e);
407 | case 4096:
408 | return new Color(0x65da92);
409 | case 8192:
410 | return new Color(0x5abc65);
411 | case 16384:
412 | return new Color(0x248c51);
413 | default:
414 | return new Color(0x248c51);
415 | }
416 | }
417 |
418 | public Font getTileFont() {
419 | int index = value < 100 ? 1 : value < 1000 ? 2 : value < 10000 ? 3 : 4;
420 | return fonts[index];
421 | }
422 | }
423 |
424 | }
425 |
--------------------------------------------------------------------------------
/src/com/game/view/Window2.java:
--------------------------------------------------------------------------------
1 | package com.game.view;
2 |
3 | import com.game.main.DirectEnum;
4 | import com.game.main.WaveThread;
5 |
6 | import javax.swing.*;
7 | import java.awt.*;
8 | import java.awt.event.KeyEvent;
9 | import java.awt.event.KeyListener;
10 | import java.util.ArrayList;
11 | import java.util.List;
12 | import java.util.Random;
13 |
14 | /**
15 | * 这个版本加入了音效、移动特效、合并特效
16 | *
17 | * @author 锋宇
18 | * @contact QQ群399643539
19 | * @website http://www.wolfbe.com
20 | * @copyright 版权归朗度云所有
21 | */
22 | public class Window2 extends JFrame {
23 |
24 | private static int score = 0; //分数
25 | final Font[] fonts = {new Font("Helvetica Neue", Font.BOLD, 48)
26 | , new Font("Helvetica Neue", Font.BOLD, 42)
27 | , new Font("Helvetica Neue", Font.BOLD, 36)
28 | , new Font("Helvetica Neue", Font.BOLD, 30)
29 | , new Font("Helvetica Neue", Font.BOLD, 24)
30 | };
31 |
32 | private GameBoard gameBoard;
33 | private JLabel ltitle;
34 | private JLabel lsctip;
35 | private JLabel lscore;
36 | private JLabel lgatip;
37 |
38 | public Window2() {
39 | this.setLayout(null);
40 | }
41 |
42 | public void initView() {
43 | ltitle = new JLabel("2048", JLabel.CENTER);
44 | ltitle.setFont(new Font("", Font.BOLD, 50));
45 | ltitle.setForeground(new Color(0x776e65));
46 | ltitle.setBounds(0, 0, 120, 60);
47 |
48 | lsctip = new JLabel("SCORE", JLabel.CENTER);
49 | lsctip.setFont(new Font("", Font.BOLD, 16));
50 | lsctip.setForeground(new Color(0xeee4da));
51 | lsctip.setOpaque(true);//只有设置为true背景色才生效
52 | lsctip.setBackground(new Color(0xbbada0));
53 | lsctip.setBounds(290, 5, 100, 25);
54 |
55 | lscore = new JLabel("0", JLabel.CENTER);
56 | lscore.setFont(new Font("Helvetica Neue", Font.BOLD, 22));
57 | lscore.setForeground(Color.WHITE);
58 | lscore.setOpaque(true);
59 | lscore.setBackground(new Color(0xbbada0));
60 | lscore.setBounds(290, 30, 100, 25);
61 |
62 | lgatip = new JLabel("按方向键可以控制方块的移动,按ESC键可以重新开始游戏。", JLabel.LEFT);
63 | lgatip.setFont(new Font("Helvetica Neue", Font.ITALIC, 13));
64 | lgatip.setForeground(new Color(0x776e65));
65 | lgatip.setBounds(10, 60, 390, 30);
66 | //游戏面板组件
67 | gameBoard = new GameBoard();
68 | gameBoard.setPreferredSize(new Dimension(400, 400));
69 | gameBoard.setBackground(new Color(0xbbada0));
70 | gameBoard.setBounds(0, 100, 400, 400);
71 | gameBoard.setFocusable(true);
72 | //把组件加入窗体
73 | this.add(ltitle);
74 | this.add(lsctip);
75 | this.add(lscore);
76 | this.add(lgatip);
77 | this.add(gameBoard);
78 | }
79 |
80 | class GameBoard extends JPanel implements KeyListener {
81 | private static final int GAP_TILE = 16; //瓦片之间间隙
82 | private static final int ARC_TILE = 16; //瓦片圆角弧度
83 | private static final int SIZE_TILE = 80;//瓦片的大小
84 | private static final int PAINT_NUM = 20;//移动的过程分成X段绘制
85 |
86 | private Tile[][] tiles = new Tile[4][4];
87 | private Color bgColor;
88 | private boolean isOver;
89 | private boolean isMove;
90 | private boolean isMerge;
91 | private boolean isAnimate;
92 |
93 | public GameBoard() {
94 | initGame();
95 | bgColor = new Color(0xbbada0);
96 | addKeyListener(this);
97 | }
98 |
99 | @Override
100 | public void keyPressed(KeyEvent e) {
101 | boolean moved = false;
102 | switch (e.getKeyCode()) {
103 | case KeyEvent.VK_ESCAPE:
104 | initGame();
105 | break;
106 | case KeyEvent.VK_LEFT:
107 | moved = moveLeft();
108 | invokeAnimate();
109 | checkGameOver(moved);
110 | break;
111 | case KeyEvent.VK_RIGHT:
112 | moved = moveRight();
113 | invokeAnimate();
114 | checkGameOver(moved);
115 | break;
116 | case KeyEvent.VK_UP:
117 | moved = moveUp();
118 | invokeAnimate();
119 | checkGameOver(moved);
120 | break;
121 | case KeyEvent.VK_DOWN:
122 | moved = moveDown();
123 | invokeAnimate();
124 | checkGameOver(moved);
125 | break;
126 | }
127 | repaint();
128 | }
129 |
130 | private void initGame() {
131 | //初始化地图
132 | for (int i = 0; i < 4; i++) {
133 | for (int j = 0; j < 4; j++) {
134 | tiles[i][j] = new Tile();
135 | }
136 | }
137 | //生成两个瓦片
138 | createTile();
139 | createTile();
140 |
141 | isOver = false;
142 | isAnimate = true;
143 | }
144 |
145 | private void createTile() {
146 | //获取当前空白的瓦片,并加入列表
147 | List list = getBlankTiles();
148 | if (!list.isEmpty()) {
149 | Random random = new Random();
150 | int index = random.nextInt(list.size());
151 | Tile tile = list.get(index);
152 | //初始化新瓦片的值为2或4
153 | tile.value = random.nextInt(100) > 50 ? 4 : 2;
154 | }
155 | }
156 |
157 | /**
158 | * 获取当前空白的瓦片,加入列表返回
159 | *
160 | * @return
161 | */
162 | private List getBlankTiles() {
163 | List list = new ArrayList();
164 | for (int i = 0; i < 4; i++) {
165 | for (int j = 0; j < 4; j++) {
166 | if (tiles[i][j].value == 0) {
167 | list.add(tiles[i][j]);
168 | }
169 | }
170 | }
171 | return list;
172 | }
173 |
174 | private void checkGameOver(boolean moved) {
175 | lscore.setText(score + "");
176 | if (!getBlankTiles().isEmpty()) {
177 | return;
178 | }
179 | for (int i = 0; i < 3; i++) {
180 | for (int j = 0; j < 3; j++) {
181 | //判断是否存在可合并的两个瓦片
182 | if (tiles[i][j].value == tiles[i][j + 1].value || tiles[i][j].value == tiles[i + 1][j].value) {
183 | isOver = false;
184 | return;
185 | }
186 | }
187 | }
188 | isOver = true;
189 | }
190 |
191 | private boolean moveLeft() {
192 | isMove = false;
193 | for (int i = 0; i < 4; i++) {
194 | for (int j = 1; j < 4; j++) {
195 | int k = j;
196 | //当前移动瓦片不能到达边界,不能为空白瓦片,前方瓦片不能是合成瓦片
197 | while (k > 0 && tiles[i][k].value != 0 && !tiles[i][k - 1].ismerge) {
198 | if (tiles[i][k - 1].value == 0) {
199 | doMove(tiles[i][k], tiles[i][k - 1], DirectEnum.LEFT);
200 | } else if (tiles[i][k - 1].value == tiles[i][k].value) {
201 | doMerge(tiles[i][k], tiles[i][k - 1], DirectEnum.LEFT);
202 | break;
203 | } else {
204 | break;
205 | }
206 | k--;
207 | }
208 | }
209 | }
210 | return isMove;
211 | }
212 |
213 | private boolean moveRight() {
214 | isMove = false;
215 | for (int i = 0; i < 4; i++) {
216 | for (int j = 2; j > -1; j--) {
217 | int k = j;
218 | //当前移动瓦片不能到达边界,不能为空白瓦片,前方瓦片不能是合成瓦片
219 | while (k < 3 && tiles[i][k].value != 0 && !tiles[i][k + 1].ismerge) {
220 | if (tiles[i][k + 1].value == 0) {
221 | doMove(tiles[i][k], tiles[i][k + 1], DirectEnum.RIGHT);
222 | } else if (tiles[i][k + 1].value == tiles[i][k].value) {
223 | doMerge(tiles[i][k], tiles[i][k + 1], DirectEnum.RIGHT);
224 | break;
225 | } else {
226 | break;
227 | }
228 | k++;
229 | }
230 | }
231 | }
232 | return isMove;
233 | }
234 |
235 | private boolean moveUp() {
236 | isMove = false;
237 | for (int j = 0; j < 4; j++) {
238 | for (int i = 1; i < 4; i++) {
239 | int k = i;
240 | //当前移动瓦片不能到达边界,不能为空白瓦片,前方瓦片不能是合成瓦片
241 | while (k > 0 && tiles[k][j].value != 0 && !tiles[k - 1][j].ismerge) {
242 | if (tiles[k - 1][j].value == 0) {
243 | doMove(tiles[k][j], tiles[k - 1][j], DirectEnum.UP);
244 | } else if (tiles[k - 1][j].value == tiles[k][j].value) {
245 | doMerge(tiles[k][j], tiles[k - 1][j], DirectEnum.UP);
246 | break;
247 | } else {
248 | break;
249 | }
250 | k--;
251 | }
252 | }
253 | }
254 | return isMove;
255 | }
256 |
257 | private boolean moveDown() {
258 | isMove = false;
259 | for (int j = 0; j < 4; j++) {
260 | for (int i = 2; i > -1; i--) {
261 | int k = i;
262 | //当前移动瓦片不能到达边界,不能为空白瓦片,前方瓦片不能是合成瓦片
263 | while (k < 3 && tiles[k][j].value != 0 && !tiles[k + 1][j].ismerge) {
264 | if (tiles[k + 1][j].value == 0) {
265 | doMove(tiles[k][j], tiles[k + 1][j], DirectEnum.DOWN);
266 | } else if (tiles[k + 1][j].value == tiles[k][j].value) {
267 | doMerge(tiles[k][j], tiles[k + 1][j], DirectEnum.DOWN);
268 | break;
269 | } else {
270 | break;
271 | }
272 | k++;
273 | }
274 | }
275 | }
276 | return isMove;
277 | }
278 |
279 | private void doMove(Tile src, Tile dst, DirectEnum directEnum) {
280 | dst.swap(src);
281 | dst.step++;
282 | dst.directEnum = directEnum;
283 | src.clear();
284 | isMove = true;
285 | }
286 |
287 | private void doMerge(Tile src, Tile dst, DirectEnum directEnum) {
288 | dst.value = dst.value << 1;
289 | dst.ismerge = true;
290 | dst.step++;
291 | dst.directEnum = directEnum;
292 | src.clear();
293 | score += dst.value;
294 | isMove = true;
295 | isMerge = true;
296 | }
297 |
298 | /**
299 | * 绘制动画效果
300 | */
301 | private void invokeAnimate() {
302 | if (isMerge) {
303 | new WaveThread("merge.wav").start();
304 | moveAnimate();
305 | mergeAnimate();
306 | } else if (isMove) {
307 | new WaveThread("move.wav").start();
308 | moveAnimate();
309 | }
310 | if (isMerge || isMove) {
311 | createTile();
312 | isMerge = false;
313 | isMove = false;
314 | }
315 | }
316 |
317 | private void mergeAnimate() {
318 | isAnimate = false;
319 | Graphics gg = getGraphics();
320 | Image image = this.createImage(getWidth(), getHeight());
321 | Graphics g = image.getGraphics();
322 | g.setColor(bgColor);
323 | g.fillRect(0, 0, getWidth(), getHeight());
324 | int k = -3; //使下面的ex从0开始
325 | while (k < 4) {
326 | int ex = 9 - k ^ 2; //抛物线公式
327 | for (int i = 0; i < 4; i++) {
328 | for (int j = 0; j < 4; j++) {
329 | if (!tiles[i][j].ismerge) {
330 | drawTile(g, i, j, 0, 0, 0);
331 | } else {
332 | drawTile(g, i, j, 0, 0, ex);
333 | }
334 | }
335 | }
336 | gg.drawImage(image, 0, 0, null);
337 | k++;
338 | }
339 | for (int i = 0; i < 4; i++) {
340 | for (int j = 0; j < 4; j++) {
341 | tiles[i][j].ismerge = false;
342 | }
343 | }
344 | isAnimate = true;
345 | }
346 |
347 | private void moveAnimate() {
348 | isAnimate = false;
349 | Graphics gg = getGraphics();
350 | Image image = this.createImage(getWidth(), getHeight());
351 | Graphics g = image.getGraphics();
352 | g.setColor(bgColor);
353 | g.fillRect(0, 0, getWidth(), getHeight());
354 | int k = 0; //
355 | while (k < PAINT_NUM) {
356 | for (int i = 0; i < 4; i++) {
357 | for (int j = 0; j < 4; j++) {
358 | int step = (GAP_TILE + SIZE_TILE) * tiles[i][j].step / PAINT_NUM;
359 | switch (tiles[i][j].directEnum) {
360 | case LEFT:
361 | drawTile(g, i, j, (PAINT_NUM - k) * step, 0, 0);
362 | break;
363 | case RIGHT:
364 | drawTile(g, i, j, (k - PAINT_NUM) * step, 0, 0);
365 | break;
366 | case UP:
367 | drawTile(g, i, j, 0, (PAINT_NUM - k) * step, 0);
368 | break;
369 | case DOWN:
370 | drawTile(g, i, j, 0, (k - PAINT_NUM) * step, 0);
371 | break;
372 | case NONE:
373 | drawTile(g, i, j, 0, 0, 0);
374 | break;
375 | }
376 | }
377 | }
378 | gg.drawImage(image, 0, 0, null);
379 | k++;
380 | }
381 | for (int i = 0; i < 4; i++) {
382 | for (int j = 0; j < 4; j++) {
383 | tiles[i][j].step = 0;
384 | tiles[i][j].directEnum = DirectEnum.NONE;
385 | }
386 | }
387 | isAnimate = true;
388 | }
389 |
390 | @Override
391 | public void paint(Graphics g) {
392 | super.paint(g);
393 | if (isAnimate) {
394 | for (int i = 0; i < 4; i++) {
395 | for (int j = 0; j < 4; j++) {
396 | drawTile(g, i, j, 0, 0, 0);
397 | }
398 | }
399 | }
400 | if (isOver) {
401 | g.setColor(new Color(255, 255, 255, 180));
402 | g.fillRect(0, 0, getWidth(), getHeight());
403 | g.setColor(new Color(0x3d79ca));
404 | g.setFont(fonts[0]);
405 | FontMetrics fms = getFontMetrics(fonts[0]);
406 | String value = "Game Over";
407 | g.drawString(value, (getWidth() - fms.stringWidth(value)) / 2, getHeight() / 2);
408 | }
409 |
410 | }
411 |
412 | private void drawTile(Graphics gg, int i, int j, int mx, int my, int ex) {
413 | Graphics2D g = (Graphics2D) gg;
414 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
415 | RenderingHints.VALUE_ANTIALIAS_ON);
416 | g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
417 | RenderingHints.VALUE_STROKE_NORMALIZE);
418 | Tile tile = tiles[i][j];
419 | //绘制瓦片背景
420 | g.setColor(tile.getBackground());
421 | //注意:横坐标用j计算,纵坐标用i计算
422 | g.fillRoundRect(GAP_TILE + (GAP_TILE + SIZE_TILE) * j + mx - ex,
423 | GAP_TILE + (GAP_TILE + SIZE_TILE) * i + my - ex,
424 | SIZE_TILE + ex * 2, SIZE_TILE + ex * 2, ARC_TILE, ARC_TILE);
425 | //绘制瓦片文字
426 | g.setColor(tile.getForeground());
427 | Font font = tile.getTileFont();
428 | g.setFont(font);
429 | FontMetrics fms = getFontMetrics(font);
430 | String value = String.valueOf(tile.value);
431 | //注意:横坐标用j计算,纵坐标用i计算
432 | g.drawString(value, GAP_TILE + (GAP_TILE + SIZE_TILE) * j
433 | + (SIZE_TILE - fms.stringWidth(value)) / 2 + mx - ex
434 | , GAP_TILE + (GAP_TILE + SIZE_TILE) * i
435 | + (SIZE_TILE - fms.getAscent() - fms.getDescent()) / 2 + my - ex
436 | + fms.getAscent());
437 | }
438 |
439 | @Override
440 | public void keyTyped(KeyEvent e) {
441 |
442 | }
443 |
444 | @Override
445 | public void keyReleased(KeyEvent e) {
446 |
447 | }
448 | }
449 |
450 | class Tile {
451 | public int step; //移动的步数
452 | public int value;//显示的数字
453 | public DirectEnum directEnum;//移动的方向
454 | public boolean ismerge;//是否是合并的
455 |
456 | public Tile() {
457 | clear();
458 | }
459 |
460 | public void clear() {
461 | step = 0;
462 | value = 0;
463 | ismerge = false;
464 | directEnum = DirectEnum.NONE;
465 | }
466 |
467 | public void swap(Tile tile) {
468 | this.step = tile.step;
469 | this.value = tile.value;
470 | this.ismerge = tile.ismerge;
471 | this.directEnum = tile.directEnum;
472 | }
473 |
474 | public Color getForeground() {
475 | switch (value) {
476 | case 0:
477 | return new Color(0xcdc1b4);
478 | case 2:
479 | case 4:
480 | return new Color(0x776e65);
481 | default:
482 | return new Color(0xf9f6f2);
483 | }
484 | }
485 |
486 | public Color getBackground() {
487 | switch (value) {
488 | case 0:
489 | return new Color(0xcdc1b4);
490 | case 2:
491 | return new Color(0xeee4da);
492 | case 4:
493 | return new Color(0xede0c8);
494 | case 8:
495 | return new Color(0xf2b179);
496 | case 16:
497 | return new Color(0xf59563);
498 | case 32:
499 | return new Color(0xf67c5f);
500 | case 64:
501 | return new Color(0xf65e3b);
502 | case 128:
503 | return new Color(0xedcf72);
504 | case 256:
505 | return new Color(0xedcc61);
506 | case 512:
507 | return new Color(0xedc850);
508 | case 1024:
509 | return new Color(0xedc53f);
510 | case 2048:
511 | return new Color(0xedc22e);
512 | case 4096:
513 | return new Color(0x65da92);
514 | case 8192:
515 | return new Color(0x5abc65);
516 | case 16384:
517 | return new Color(0x248c51);
518 | default:
519 | return new Color(0x248c51);
520 | }
521 | }
522 |
523 | public Font getTileFont() {
524 | int index = value < 100 ? 1 : value < 1000 ? 2 : value < 10000 ? 3 : 4;
525 | return fonts[index];
526 | }
527 | }
528 |
529 | }
530 |
--------------------------------------------------------------------------------
/src/resources/merge.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beyondfengyu/G2048/9b88a0b2451321dcd793f8405b2d5a3e19b6cfd9/src/resources/merge.wav
--------------------------------------------------------------------------------
/src/resources/move.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beyondfengyu/G2048/9b88a0b2451321dcd793f8405b2d5a3e19b6cfd9/src/resources/move.wav
--------------------------------------------------------------------------------