整体框架
实现蛇的头部
如何画蛇?
新建Cell.js
,用于表示一个蛇的方块。
1 2 3 4 5 6 7 8 9 export class Cell { constructor (r, c ) { this .r = r; this .c = c; this .x = c + 0.5 ; this .y = r + 0.5 ; } }
新建Snake.js
对象
cells数组就用于存放蛇的“身体”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { AcGameObject } from "./AcGameObject" ;import { Cell } from "./Cell" ;export class Snake extends AcGameObject { constructor (info, gamemap ) { super (); this .id = info.id ; this .color = info.color ; this .gamemap = gamemap; this .cells = [new Cell (info.r , info.c )]; } start ( ) { } update ( ) { this .render (); } render ( ) { const L = this .gamemap .L ; const ctx = this .gamemap .ctx ; ctx.fillStyle = this .color ; for (const cell of this .cells ) { ctx.beginPath (); ctx.arc (cell.x * L, cell.y * L, L / 2 , 0 , Math .PI * 2 ); ctx.fill (); } } }
在GameMap.js
中创建两条蛇的对象
1 2 3 4 this .snakes = { new Snake ({id : 0 , color : "#4876ec" , r : this .rows - 2 , c : 1 }, this ), new Snake ({id : 1 , color : "#f94848" , r : 1 , c : this .cols - 2 }, this ), }
实现蛇的移动
蛇什么时候可以动?
如何移动?
除了头和尾,中间不懂。在蛇的长度增加时,尾也不用动,只需移动头。
在GameMap.js
中,对蛇是否可以进行移动进行判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 check_ready ( ) { for (const snake of this .snakes ) { if (snake.status !== "idle" ) return false ; if (snake.direction === -1 ) return false ; } return true ; } next_step ( ) { for (const snake of this .snake ) { snake.next_step (); } } update ( ) { this .update_size (); if (this .check_ready ()) { this .next_step (); } this .render (); }
修改Snake.js
,实现移动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 import { AcGameObject } from "./AcGameObject" ;import { Cell } from "./Cell" ;export class Snake extends AcGameObject { constructor (info, gamemap ) { super (); this .id = info.id ; this .color = info.color ; this .gamemap = gamemap; this .cells = [new Cell (info.r , info.c )]; this .next_cell = null ; this .speed = 5 ; this .direction = -1 ; this .status = "idle" ; this .dr = [-1 , 0 , 1 , 0 ]; this .dc = [0 , 1 , 0 , -1 ]; this .step = 0 ; this .eps = 1e-2 ; } start ( ) { } set_direction (d ) { this .direction = d; } next_step ( ) { const d = this .direction ; this .next_cell = new Cell (this .cells [0 ].r + this .dr [d], this .cells [0 ].c + this .dc [d]); this .direction = -1 ; this .status = "move" ; this .step ++ ; const k = this .cells .length ; for (let i = k; i > 0 ; i -- ) { this .cells [i] = JSON .parse (JSON .stringify (this .cells [i - 1 ])); } } update_move ( ) { const dx = this .next_cell .x - this .cells [0 ].x ; const dy = this .next_cell .y - this .cells [0 ].y ; const distance = Math .sqrt (dx * dx + dy * dy); if (distance < this .eps ) { this .cells [0 ] = this .next_cell ; this .next_cell = null ; this .status = "idle" ; } else { const move_distance = this .speed * this .timedelta / 1000 ; this .cells [0 ].x += move_distance * dx / distance; this .cells [0 ].y += move_distance * dy / distance; } } update ( ) { if (this .status === 'move' ) { this .update_move (); } this .render (); } render ( ) { const L = this .gamemap .L ; const ctx = this .gamemap .ctx ; ctx.fillStyle = this .color ; for (const cell of this .cells ) { ctx.beginPath (); ctx.arc (cell.x * L, cell.y * L, L / 2 , 0 , Math .PI * 2 ); ctx.fill (); } } }
利用键盘控制移动
首先修改GameMap.vue
,令canvas能够获取键盘事件
1 <canvas ref ="canvas" tabindex ="0" > </canvas >
在Snake.js
中加入一个辅助函数,用于修改蛇的前进方向。
1 2 3 set_direction (d ) { this .direction = d; }
在GameMap.js
中添加监听事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 add_listening_events ( ) { this .ctx .canvas .focus (); const [snake0, snake1] = this .snakes ; this .ctx .canvas .addEventListener ("keydown" , e => { if (e.key === 'w' ) snake0.set_direction (0 ); else if (e.key === 'd' ) snake0.set_direction (1 ); else if (e.key === 's' ) snake0.set_direction (2 ); else if (e.key === 'a' ) snake0.set_direction (3 ); else if (e.key === 'ArrowUp' ) snake1.set_direction (0 ); else if (e.key === 'ArrowRight' ) snake1.set_direction (1 ); else if (e.key === 'ArrowDown' ) snake1.set_direction (2 ); else if (e.key === 'ArrowLeft' ) snake1.set_direction (3 ); }); }
蛇尾状态更新
需要一个函数判断蛇的长度是否增加。如果蛇的长度增加,则蛇尾不用更新。
1 2 3 4 5 check_tail_increasing ( ) { if (step <= 10 ) return true ; if (step % 3 === 1 ) return true ; return false ; }
怎么更新蛇尾呢?跟蛇头几乎一模一样。不过在蛇完成当前移动后,更新蛇尾多出了一步,那就是删除掉当前的蛇尾。
这块先别管,首先pop是需要的,因为长度不会增加,蛇头是新增的,所以肯定需要删除一个。但是下面更新蛇尾的操作或许是鸡肋的,并未发现任何逻辑意义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 if (distance < this .eps ) { this .cells [0 ] = this .next_cell ; this .next_cell = null ; this .status = "idle" ; if (!this .check_tail_increasing ()) { this .cells .pop (); } } else { const move_distance = this .speed * this .timedelta / 1000 ; this .cells [0 ].x += move_distance * dx / distance; this .cells [0 ].y += move_distance * dy / distance; if (!this .check_tail_increasing ()) { const k = this .cells .length ; const tail = this .cells [k - 1 ], tail_target = this .cells [k - 2 ]; const tail_dx = tail_target.x - tail.x ; const tail_dy = tail_target.y - tail.y ; tail.x += move_distance * tail_dx / distance; tail.y += move_distance * tail_dy / distance; } }
美化蛇
蛇的身体
此时的蛇是一堆圆连在一起,很难看。我们可以适当缩小圆的大小,并在圆和圆之间上覆盖一个矩形,这样就能使蛇组成一个类似于操场的形状,相对比要好看多了。
修改Snake.js
的render
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 render ( ) { const L = this .gamemap .L ; const ctx = this .gamemap .ctx ; ctx.fillStyle = this .color ; for (const cell of this .cells ) { ctx.beginPath (); ctx.arc (cell.x * L, cell.y * L, L / 2 * 0.8 , 0 , Math .PI * 2 ); ctx.fill (); } for (let i = 1 ; i < this .cells .length ; i++) { const a = this .cells [i - 1 ], b = this .cells [i]; if (Math .abs (a.x - b.x ) < this .eps && Math .abs (a.y - b.y ) < this .eps ) continue ; if (Math .abs (a.x - b.x ) < this .eps ) { ctx.fillRect ((a.x - 0.4 ) * L, Math .min (a.y , b.y ) * L, L * 0.8 , Math .abs (a.y - b.y ) * L); } else { ctx.fillRect (Math .min (a.x , b.x ) * L, (a.y - 0.4 ) * L, Math .abs (a.x - b.x ) * L, L * 0.8 ); } } }
蛇的眼睛
利用canvas给蛇头上画两个黑色的小圆充当眼睛。眼睛根据蛇移动的方向,和蛇头的偏移量均不同,需要提前打个表。
修改Snake.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 this .eye_direction = 0 ;if (this .id === 1 ) this .eye_direction = 2 ;this .eye_dx = [ [-1 , 1 ]; [1 , 1 ]; [1 , -1 ]; [-1 , -1 ]; ]; this .eye_dy = [ [-1 , -1 ]; [-1 , 1 ]; [1 , 1 ]; [1 , -1 ]; ]; next_step ( ) { this .eye_direction = d; } render ( ) { ctx.fillStyle = "black" ; for (let i = 0 ; i < 2 ; i++) { const eye_x = (this .cells [0 ].x + this .eye_dx [this .eye_direction ][i] * 0.15 ) * L; const eye_y = (this .cells [0 ].y + this .eye_dy [this .eye_direction ][i] * 0.15 ) * L; ctx.beginPath (); ctx.arc (eye_x, eye_y, L * 0.05 , 0 , Math .PI * 2 ); ctx.fill (); } }
蛇的死亡
如何实现蛇的死亡?
下一步玩家/后端给出的操作是非法操作!
非法操作有:撞墙/障碍物, 撞蛇(自己/对方)
为了表示蛇的死亡状态,可以把此时蛇的颜色设置为白色。
在GameMap.js
中修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 check_valid (cell ) { for (const wall of this .walls ) { if (wall.r === cell.r && wall.c === cell.c ) return false ; } for (const snake of this .snakes ) { let k = snake.cells .length ; if (!snake.check_tail_increasing ()) { k--; } for (let i = 0 ; i < k; i++) { if (snake.cells [i].r === cell.r && snake.cells [i].c === cell.c ) return false ; } } return true ; }
在Snake.js
中修改
1 2 3 4 5 6 7 8 9 10 11 next_step ( ) { if (!this .gamemap .check_valid (this .next_cell )) { this .status = "die" ; } } render ( ) { if (this .status === "die" ) { ctx.fillStyle = "white" ; } }
最终效果
版权声明: 此文章版权归金晖のBlog所有,如有转载,请注明来自原作者