├── 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 | ![2048](http://res.img.wolfbe.com/lst/1474644636299.png) 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 --------------------------------------------------------------------------------