前言

《Pac-Man》上一次已经写过了,但是用vue写的,整体下来能玩,但是自己感觉不算流畅,而且游戏计算方面也有点欠缺,感兴趣的可看我上一篇文章。这次我又卷土重来,在三四天内用空闲时间,用canvas重构了一个完整且自认为完美的Pac-Man,无论是流畅度还是音乐,自测都感觉比较…perfect…

又因为此次中的代码还是比较繁琐的,我就不一一贴代码了,我就把整个游戏的做的过程走一遍,以及其中要注意的部分都会讲解到位。

此次游戏做的过程中,其实真的写起来还是有很多不明白的,自己也是慢慢的查阅、调试、询问等方式去慢慢啃下来。

废话不多说,直接走流程。

一、游戏代码分析

游戏展示用canvas绘制

<canvas id="myCanvas"></canvas>

游戏的核心代码就是用class类来分解游戏中各个元素

  • Direction.js —— 方向
  • Game.js —— 游戏入口
  • Ghost.js —— 幽灵
  • Pacman.js —— 吃豆人
  • TileMap.js —— 地图

在核心代码class中,为了好区分,又把方法分为公共方法(不加#)和私有方法(加#)
例如:

draw() { ... }
#move() { ... }

二、游戏所需文件


(从上往下)

  1. 音乐文件(背景、死亡、吃金币、吃幽灵、吃闪光金币、获胜,全部用的是超级玛丽的音乐)
  2. 图片文件(墙壁砖、吃豆人的整个动效图片、幽灵本体、幽灵闪烁图片、金币图片、闪光金币图片)
  3. 核心文件(上面说的5个js文件)
  4. 样式文件(index.css)
  5. 入口文件(index.html)

三、游戏基本布局

1. 页面布局

<div id="app"><!-- 游戏名 --><h1 class="title">Pac-Man</h1><!-- 游戏区 --><div class="game"><canvas id="myCanvas"></canvas></div>
</div>
<script src="js/Game.js" type="module"></script>

2. 样式

* { padding: 0; margin: 0; }
#app {width: 100%;height: 100vh;padding-top: 100px;position: fixed;display: flex;flex-direction: column;align-items: center;font-family: comic sans MS;background: linear-gradient(0deg, rgb(17, 51,161) 0%, rgb(136,34,195) 100%);
}
.title {color: lightgray;margin-bottom: 30px;font-size: 50px;user-select: none;
}.game {background-color: #000;
}#myCanvas {display: block;box-shadow: 10px 10px 20px black;
}

四、聊聊游戏核心代码

1. 入口类 Game.js

这个类的作用,主要就是存放游戏的基本配置信息,比如获取canvas,游戏定时器,游戏胜利、失败等

(1)在游戏开始,我们需要获取canvas

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

(2)还要配置需要的基本参数

const tileSize = 30; // 砖块尺寸(每个格子的长宽)
const speed = 2;     // 速度
const gameOverAudio = new Audio('../audio/die.wav');  // 游戏结束音效
const gameWinAudio = new Audio('../audio/win.wav');   // 游戏胜利音效
const bgAudio = new Audio('../audio/bg.mp3');         // 游戏背景音乐
let gameOver = false; // 游戏是否结束
let gameWin = false;  // 游戏是否胜利

(3)又因为我们的游戏是高灵活性的,可以随意搭建自己想玩的地图,所以我们的canvas也要根据地图来生成大小

// 设置地图默认大小
tileMap.setCanvasSize(canvas);

(4)页面加载时,游戏需要一个定时器,来实时监听页面的变化

// 游戏定时器
setInterval(gameLoop, 1000 / 75);// 游戏运行
function gameLoop() {ctx.clearRect(0, 0, canvas.width, canvas.height);   // 每次先清空画布tileMap.draw(ctx);                                  // 画地图pacman.draw(ctx, pause(), ghost);                   // 画吃豆人ghost.forEach(e => e.draw(ctx, pause(), pacman));   // 画幽灵checkGameOver();  // 检查是否结束checkGameWin();   // 检查是否获胜drawGameDraw();   // 画游戏结束的提示框// 只有吃豆人动了,背景音乐才播放,游戏结束则停止播放if(pacman.madeFirstMove && !gameOver && !gameWin) {bgAudio.play();}
}

上面的个别代码比如传参啊,比如方法的内容,后面我们都会一一说明,因为这几个类基本上都会互相调用,要是放一起就会显得很乱,只能单拎出来说,最后再放一起整合就好了。

(5)定义暂停函数

// 是否暂停
// 如果 吃豆人没动,或者 游戏输了或者赢了,游戏都会处于暂停状态
function pause() {return !pacman.madeFirstMove || gameOver || gameWin;
}

(6)判断游戏是否结束

其实就是根据2D碰撞检测,检测幽灵是否碰到吃豆人了

下面是MDN官网的链接,感兴趣可以瞅瞅。

[MDN-web-docs](2D 碰撞检测 - 游戏开发环境 | MDN (mozilla.org))

function checkGameOver() {if(!gameOver) {// powerCoinActive:吃豆人是否吃到闪光豆豆// collidePacMan: 幽灵里面有没有哪个幽灵与吃豆人发生碰撞gameOver = ghost.some(item => !pacman.powerCoinActive && item.collidePacMan(pacman));if(gameOver) {bgAudio.pause();gameOverAudio.play();}}
}

(7)判断游戏是否赢了

找二维数组地图数据中只要没有金币即可

function checkGameWin() {if(!gameWin) {gameWin = tileMap.isWin();if(gameWin) {bgAudio.pause();gameWinAudio.play();}}
}

(8)游戏结束提示框

function drawGameDraw() {if(gameOver || gameWin) {let txt = 'You Win!'if(gameOver) {txt = 'Game Over!'}ctx.fillStyle = 'black';ctx.fillRect(0, canvas.height / 2.5, canvas.width, 100);ctx.font = '80px comic sans';// 渐变  官网有示例  直接拿来用const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);gradient.addColorStop('0', 'magenta');gradient.addColorStop('0.5', 'blue');gradient.addColorStop('1.0', 'red');ctx.fillStyle = gradient;ctx.textAlign = 'center';ctx.fillText(txt, canvas.width / 2, canvas.height / 1.75);}
}

2. 地图类 TileMap.js

TileMap主要存放关于地图的一切,墙壁、豆豆、闪光豆豆、判断撞墙、是否吃到豆豆、还有上面说的是否胜利等,为了能实时获取吃豆人和幽灵的位置信息,他们也是在这个里面获取的

(1)定义基本参数

constructor(tileSize) {this.tileSize = tileSize;// 金币this.coinDot = new Image();this.coinDot.src = "../img/coin1.png";// 闪光金币this.pinkCoinDot = new Image();this.pinkCoinDot.src = "../img/coinPink.png";// 墙this.wall = new Image();this.wall.src = "../img/wall.png";// 闪光金币定时器,说白一点,就是频繁切换金币图片,有闪烁的效果this.powerCoinTimerDefault = 40;this.powerCoinTimer = this.powerCoinTimerDefault;this.powerCoinDot = this.coinDot;}

(2)定义地图数据

依旧是一个二维数组

// 0 = 金币
// 1 = 墙
// 4 = 吃豆人
// 5 = 空地
// 6 = 幽灵
// 7 = 闪光金币
map = [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,6,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,4,1],[1,0,1,0,1,1,1,1,0,1,1,1,1,0,1,0,0,1,0,1],[1,0,1,0,0,0,0,1,0,0,0,0,1,0,1,0,0,1,0,1],[1,0,1,0,0,0,0,1,0,0,0,0,1,0,1,0,0,1,0,1],[1,0,1,0,0,0,0,1,0,0,0,0,1,0,1,0,0,1,0,1],[1,0,1,0,0,0,6,1,0,0,0,0,1,0,1,0,0,1,0,1],[1,7,1,0,1,1,1,1,0,1,1,1,1,0,1,1,1,1,7,1],[1,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,7,1,0,1],[1,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,1,0,1],[1,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,1,0,1],[1,0,1,0,1,0,0,0,0,0,0,6,1,0,0,0,0,1,0,1],[1,0,1,0,1,1,1,1,0,1,1,1,1,0,0,0,0,1,0,1],[1,6,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,6,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]

都定义好了,接下来就是画基本游戏界面了

(3)确定canvas的长宽

setCanvasSize(canvas) {canvas.width = this.map[0].length * this.tileSize;canvas.height = this.map.length * this.tileSize;
}

(4)绘制地图

draw(ctx) {for(let i = 0; i < this.map.length; i++) {for(let j = 0; j < this.map[0].length; j++) {let tile = this.map[i][j];if(tile === 1) {this.#drawWall(ctx, i, j, this.tileSize);} else if(tile === 0) {this.#drawCoin(ctx, i, j, this.tileSize);} else if(tile === 7) {this.#drawSuperCoin(ctx, i, j, this.tileSize)} else {this.#drawBlank(ctx, i, j, this.tileSize);}}}
}// 绘制墙
#drawWall(ctx, row, col, size) {ctx.drawImage(this.wall, col * size, row * size, size, size);
}// 绘制金币
#drawCoin(ctx, row, col, size) {ctx.drawImage(this.coinDot, col * size, row * size, size, size);
}// 绘制超级金币(buffer: 使怪物弱化)
#drawSuperCoin(ctx, row, col, size) {this.powerCoinTimer--;if(this.powerCoinTimer == 0) {this.powerCoinTimer = this.powerCoinTimerDefault;if(this.powerCoinDot == this.pinkCoinDot) {this.powerCoinDot = this.coinDot;} else {this.powerCoinDot = this.pinkCoinDot;}}ctx.drawImage(this.powerCoinDot, col * size, row * size, size, size);
}// 绘制空地
#drawBlank(ctx, row, col, size) {ctx.fillStyle = 'black';ctx.fillRect(col * size, row * size, size, size);
}

(5)获取吃豆人的信息

注意这里需要引入吃豆人类,也就是吃豆人的一些基本信息

  getPacman(speed) {for(let i = 0; i < this.map.length; i++) {for(let j = 0; j < this.map[0].length; j++) {let tile = this.map[i][j];if(tile == 4) {this.map[i][j] = 0; // 无论吃豆人还是幽灵是不需要占位的,所以需要把它们那一块变成豆豆return new Pacman( j * this.tileSize, i * this.tileSize, this.tileSize, speed, this);}}}}

(6)获取幽灵

注意这里需要引入幽灵类,获取全部的幽灵,同上

  getGhost(speed) {const ghosts = [];for(let i = 0; i < this.map.length; i++) {for(let j = 0; j < this.map[0].length; j++) {const tile = this.map[i][j];if(tile === 6) {this.map[i][j] = 0;ghosts.push(new Ghost( j * this.tileSize, i * this.tileSize, this.tileSize, speed, this));}}}return ghosts;}

(7)判断撞墙

我们可以根据坐标以及方向,判断下一块是否是墙

Direction需要引入

isCollideWall(x, y, direction) {if(direction == null) {return;}// 必须是整数才可以,因为游戏里,都是一块一块的if(Number.isInteger(x / this.tileSize) && Number.isInteger(y / this.tileSize)) {let col = 0;    // 纵坐标let row = 0;    // 横坐标let nextCol = 0;  // 下一块的左距离let nextRow = 0;  // 下一块的上距离switch(direction) {case Direction.up:nextRow = y - this.tileSize;row = nextRow / this.tileSize;col = x / this.tileSize;break;case Direction.down:nextRow = y + this.tileSize;row = nextRow / this.tileSize;col = x / this.tileSize;break;case Direction.left:nextCol = x - this.tileSize;col = nextCol / this.tileSize;row = y / this.tileSize;break;case Direction.right:nextCol = x + this.tileSize;col = nextCol / this.tileSize;row = y / this.tileSize;break;}const tile = this.map[row][col];if(tile === 1) {return true;}}return false;}

(8) 吃豆豆

我们可以根据坐标来判断当前所在格子中,是否有豆豆

isEatDot(x, y) {const row = y / this.tileSize;const col = x / this.tileSize;if(Number.isInteger(row) && Number.isInteger(col)) {if(this.map[row][col] === 0) {this.map[row][col] = 5;return true;}}return false;
}

(9) 吃闪光豆豆(同上)

isEatPowerDot(x, y) {const row = y / this.tileSize;const col = x / this.tileSize;if(Number.isInteger(row) && Number.isInteger(col)) {if(this.map[row][col] === 7) {this.map[row][col] = 5;return true;}}return false;
}

(10)判断是否赢了

// flat() 方法方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回
isWin() {const len = this.map.flat().filter(item => item === 0).length;return len === 0;
}

3. 吃豆人 Pacman.js

Pacman主要存放吃豆人信息

(1)定义吃豆人基础参数

constructor(x, y, size, speed, tileMap) {this.x = x;this.y = y;this.size = size;this.speed = speed;this.tileMap = tileMap;this.currentDirection = null;                             // 当前运动的方向this.requestDirection = null;                             // 期望的方向(我想让它改变的方向)this.pacmanAnimateTimerDefault = 10;                      // 吃豆人动画  默认值this.pacmanAnimateTimer = null;                           // 吃豆人多久换下一张图片this.pacmanRotate = this.rotates.left;                    // 吃豆人旋转this.eatAudio = new Audio('../audio/eat.wav');            // 吃豆豆音效this.eatPowerAudio = new Audio('../audio/eatPower.mp3');  // 吃闪光豆豆音效this.powerCoinActive = false;                             // 是否吃到了闪光豆豆this.powerCoinAboutToExpire = false;                      // 闪光豆豆的效果是否快失效了,如果一个闪光豆豆的效果是6s,那当第3秒的时候,就让幽灵闪烁起来,提示用户快失效了this.timer = [];                                          // 存放闪光豆豆效果的计时器,和失效的计时器。this.eatGhostAudio = new Audio('../audio/eatGhost.wav');  // 吃幽灵音效this.madeFirstMove = false;                               // 吃豆人是否已经开始移动了,true移动了,这时候幽灵也可以移动了this.#loadPacmanImages();document.addEventListener('keydown', this.#keyDown);      // 监听键盘上下左右}//  旋转角度rotates = {left: 0,up: 1,right: 2,down: 3}

(2)绘制吃豆人

游戏过程中,需要对吃豆人进行暂停,所以我们在Game里定义一个pause方法。

对于吃豆人,如果游戏结束,或者未开始,它是不能动的。

还要在绘制的时候,传入幽灵数组,是为了吃豆人行动过程中,判断是否吃到幽灵

需要注意的是,在canvas里面,不能直接去对元素进行旋转,所以我们需要根据上下左右,进行旋转。这里我是从网上学到的,那就是对图像进行和整体canvas进行移动和旋转,比如可以先旋转图像,保存吃豆人的图像,然后再把canavs旋转回去,这样吃豆人就完成了旋转,这点就是比较麻烦的。

draw(ctx, pause, ghosts) {if(!pause) {this.#move();this.#pacmanAnimate();  // 吃豆人本身动画}this.#eatDot();         // 吃豆豆this.#eatPowerDot();    // 吃闪光豆豆this.#eatGhost(ghosts); // 吃幽灵// 绘制吃豆人,需要注意的重点const size = this.size / 2;ctx.save();ctx.translate(this.x + size, this.y + size);ctx.rotate((this.pacmanRotate * 90 * Math.PI) / 180);ctx.drawImage(this.pacmanImgs[this.pacmanIndex], -size, -size, this.size, this.size);ctx.restore();
}

(3)监听键盘上下左右

这里需要注意的是,我们在移动的过程中,是需要定义两个方向值,一个是当前移动方向,还有一个就是玩家希望吃豆人移动的方向,主要就是为了判断会不会撞到墙,其中相反方向不用判断,因为比如你往左走,突然又往右走,那说明刚才右边一定不会有墙。

还有一个需要注意,一个参数madeFirstMove 这个其实就是开始游戏一样,当我们按下上下左右任意一个键,游戏才会正常开始,吃豆人和幽灵也开始移动

#keyDown = (e) => {// upif(e.keyCode == 38) {if(this.currentDirection == moveDirection.down) {this.currentDirection = moveDirection.up;}this.requestDirection = moveDirection.up;this.madeFirstMove = true;}// downif(e.keyCode == 40) {if(this.currentDirection == moveDirection.up) {this.currentDirection = moveDirection.down;}this.requestDirection = moveDirection.down;this.madeFirstMove = true;}// leftif(e.keyCode == 37) {if(this.currentDirection == moveDirection.right) {this.currentDirection = moveDirection.left;}this.requestDirection = moveDirection.left;this.madeFirstMove = true;}// rightif(e.keyCode == 39) {if(this.currentDirection == moveDirection.left) {this.currentDirection = moveDirection.right;}this.requestDirection = moveDirection.right;this.madeFirstMove = true;}}

(4)吃豆人移动

根据移动方向进行+ -操作,以及移动过程中的注意事项,下面注释中都已标出

#move() {// 如果当前移动方向与期望的移动方向不一样,说明你想转弯了// 但是你还不能随便转弯,需要当吃豆人的上距离 并且左距离 正好够方块size的整数// 比如总不能走到某个墙的一半的时候转弯吧,这是不可行的if(this.currentDirection !== this.requestDirection) {// isInteger() 函数用于检测指定参数是否为无整数,如果是整数返回 true,否则返回 falseif(Number.isInteger(this.x / this.size) && Number.isInteger(this.y / this.size)) {// 判断是否撞到了墙, 当期望方向上的第一块不是墙,才可以转弯// 比如当前方向是向右的,我突然按了下,这时候就需要判断下面那一块是否有墙,有则不能转弯,没有则可以直接转弯if(!this.tileMap.isCollideWall(this.x, this.y, this.requestDirection)) {this.currentDirection = this.requestDirection;}}}// 一直朝一个方向移动,如果撞墙了,则停止,并且吃豆人动画暂停if(this.tileMap.isCollideWall(this.x, this.y, this.currentDirection)) {this.pacmanAnimateTimer = null;this.pacmanIndex = 1;return;} else if(this.currentDirection != null && this.pacmanAnimateTimer == null) {this.pacmanAnimateTimer = this.pacmanAnimateTimerDefault;}// 移动过程中,还要保证旋转角度的改变switch (this.currentDirection) {case moveDirection.up:this.y -= this.speed;this.pacmanRotate = this.rotates.up;break;case moveDirection.down:this.y += this.speed;this.pacmanRotate = this.rotates.down;break;case moveDirection.left:this.x -= this.speed;this.pacmanRotate = this.rotates.left;break;case moveDirection.right:this.x += this.speed;this.pacmanRotate = this.rotates.right;break;}
}

(5)吃豆人的张嘴动效

这个其实就是频繁的切换图片,来达到张嘴闭嘴的动画效果

#loadPacmanImages() {const pacmanImg1 = new Image();pacmanImg1.src = '../img/pacman1.png';const pacmanImg2 = new Image();pacmanImg2.src = '../img/pacman2.png';const pacmanImg3 = new Image();pacmanImg3.src = '../img/pacman3.png';const pacmanImg4 = new Image();pacmanImg4.src = '../img/pacman2.png';this.pacmanImgs = [pacmanImg1, pacmanImg2, pacmanImg3, pacmanImg4]; // 吃豆人图片数组this.pacmanIndex = 1;   // 默认
}#pacmanAnimate() {if(this.pacmanAnimateTimer == null) {return;}this.pacmanAnimateTimer--;if(this.pacmanAnimateTimer == 0) {this.pacmanAnimateTimer = this.pacmanAnimateTimerDefault;this.pacmanIndex++;if(this.pacmanIndex == this.pacmanImgs.length) {this.pacmanIndex = 0;}}}

(6)吃豆豆

在地图类里面,我们定义了是否吃到了豆豆,在这里就可以用了,每吃一个豆豆就会播放音效

#eatDot() {if(this.tileMap.isEatDot(this.x, this.y) && this.madeFirstMove) {// play() failed because the user didn't interact with the document first.// 这个错误信息,其实就是最新的浏览器告诉你,现在不能上来就播放音乐了,得需要跟播放做交互动作// 这时候只要加上我们定义的 madeFirstMove 即可,// this.eatAudio.play();}}

(6)吃闪光豆豆

吃闪光豆豆比吃豆豆需要多一个效果,就是可以弱化幽灵。在这里我们需要用定时器来对每只幽灵的效果时间进行计算,每吃一个闪光豆豆,就有6秒的弱化时间,并且当时间过去一半的时候,幽灵是需要闪烁来提示玩家,效果快要过期了

#eatPowerDot() {if(this.tileMap.isEatPowerDot(this.x, this.y)) {// 播放音效this.eatPowerAudio.play();// 弱化效果this.powerCoinActive = true;          // 效果开启this.powerCoinAboutToExpire = false;  // 这时候还不用提示快失效了// 默认先清空,防止吃完第一颗闪光豆豆后,时间还没到,又吃第二颗this.timer.forEach(timer => clearTimeout(timer));this.timer = [];//  效果一共持续时间let powerCoinActiveTimer = setTimeout(() => {this.powerCoinActive = false;this.powerCoinAboutToExpire = false;}, 1000 * 6);this.timer.push(powerCoinActiveTimer);// 时间到达一半的时候,需要提示快失效了let powerCoinAboutToExpireTimer = setTimeout(() => {    this.powerCoinAboutToExpire = true;}, 1000 * 3);this.timer.push(powerCoinAboutToExpireTimer);}
}

这里我就不贴图了,我不会做gif图片。

(7)吃幽灵

这个就只需要判断,我们吃闪光豆豆的效果期间,幽灵有没有碰到我(我有没有碰到幽灵)。碰到则就把当前幽灵从幽灵数组中删除

collidePacMan方法 在幽灵类中定义,就是用来判断,幽灵有没有碰到玩家的吃豆人,用的是2D碰撞检测方法

#eatGhost(ghosts) {if(this.powerCoinActive) {const collideGhost = ghosts.filter(item => item.collidePacMan(this));collideGhost.forEach(item => {ghosts.splice(ghosts.indexOf(item), 1);this.eatGhostAudio.play();});}}

4. 关于方向 Direction.js

这个不多说,其实就相当于定义了一个全局方向枚举

const Direction = {up: 0,down: 1,left: 2,right: 3
}export default Direction;

5. 幽灵类 Ghost.js

存放关于幽灵的一切

先分析一下幽灵,幽灵在定义好的地图中产生,然后刚开始默认是不能动的,当玩家点击上下左右的时候,这时候幽灵才可以移动,并且幽灵移动的方向是随机的,下来我们定义一个变量,来根据这个值决定,幽灵多长时间想要转变方向。幽灵的移动是随机的,幽灵碰到吃豆人,则游戏结束(没吃闪光豆豆的前提下)。 最后幽灵会改变自身的颜色(其实就是换图片),也就是被弱化到一半时间的时候,上面都说过,会有弱化时间。

(1)定义幽灵所需要的参数

constructor(x, y, size, speed, tileMap) {this.x = x;this.y = y;this.size = size;this.speed = speed;this.tileMap = tileMap;this.#laodImages();  // 加载幽灵图片this.randomMoveDir = Math.floor(Math.random() * Object.keys(Direction).length);              // 随机获取一个方向this.directionTimerDefault = this.#random(10, 30);  // 默认多长时间切换一次方向, 值越小,幽灵换方向越频繁this.directionTimer = this.directionTimerDefault;   // 实际切换时间递减,到0时,表示要切换了this.blinkAboutToExpireTimerDefault = 10;           // 常量 默认闪烁的间隔(也就是频繁的切换图片)this.blinkAboutToExpireTimer = this.blinkAboutToExpireTimerDefault; // 闪烁间隔递减,到0时,切换图片}// 拿到幽灵所需要的所有图片
#laodImages() {this.ghostImageDefault = new Image();this.ghostImageDefault.src = '../img/ghost1.png';this.ghostImageBlink1 = new Image();this.ghostImageBlink1.src = '../img/ghost2.png';this.ghostImageBlink2 = new Image();this.ghostImageBlink2.src = '../img/ghost3.png';this.ghostImageActive = this.ghostImageDefault; // 当前图片
}// 获取一个区间的随机数
#random(min, max) {return Math.floor(Math.random() * (max - min + 1)) + min;
}

(2) 绘制幽灵

游戏暂停,幽灵不能走动

draw(ctx, pause, pacman) {if(!pause) {this.#move();this.#changeDirection();}this.#setImage(ctx, pacman);
}#setImage(ctx, pacman) {// 如果吃到了闪光豆豆,则需要让幽灵变色if(pacman.powerCoinActive) {this.#setImageWhenEatPowerCoin(pacman);} else {this.ghostImageActive = this.ghostImageDefault;}ctx.drawImage(this.ghostImageActive, this.x, this.y, this.size, this.size);
}// 吃豆人吃了闪光豆豆,幽灵会立马变颜色
// 并且当时间过去一半的时候,幽灵需要频繁切换图片,达到闪烁的效果
#setImageWhenEatPowerCoin(pacman) {if(pacman.powerCoinAboutToExpire) {this.blinkAboutToExpireTimer--;if(this.blinkAboutToExpireTimer == 0) {this.blinkAboutToExpireTimer = this.blinkAboutToExpireTimerDefault;if(this.ghostImageActive == this.ghostImageBlink1) {this.ghostImageActive = this.ghostImageBlink2;} else {this.ghostImageActive = this.ghostImageBlink1;}}} else {this.ghostImageActive = this.ghostImageBlink1;}}

(3)幽灵移动

分析幽灵移动的特点,幽灵如果没到切换方向的时候,会一直往一个方向移动,直到撞到墙为止。另一个就是当切换方向时间到了,幽灵走到一半,突然换方向了。

#move() {// 如果没撞墙,则会一直走if(!this.tileMap.isCollideWall(this.x, this.y, this.randomMoveDir)) {switch (this.randomMoveDir) {case Direction.up:this.y -= this.speed;break;case Direction.down:this.y += this.speed;break;case Direction.left:this.x -= this.speed;break;case Direction.right:this.x += this.speed;break;}}}

(4)幽灵随机改变方向

每个幽灵会根据改变方向定义的时间,来定期的随机改变方向

#changeDirection() {this.directionTimer--;let newMoveDirection = null;if(this.directionTimer == 0) {this.directionTimer = this.directionTimerDefault;// 时间一到,重新随机获取一个方向newMoveDirection = Math.floor(Math.random() * Object.keys(Direction).length);}// 这里用了多重判断// 新的移动方向 不能跟随机产生的一样,没意义// 而且幽灵走的位置,刚刚好处于拐弯处// 并且新的方向上的那一块,不是墙壁if(newMoveDirection != null && this.randomMoveDir != newMoveDirection) {if(Number.isInteger(this.x / this.size) && Number.isInteger(this.y / this.size)) {if(!this.tileMap.isCollideWall(this.x, this.y, newMoveDirection)) {this.randomMoveDir = newMoveDirection;}}}
}

(5)2D碰撞检测 - 主要用于判断是否碰到吃豆人

collidePacMan(pacman) {const size = this.size / 1.5;if(this.x < pacman.x + size && this.x + size > pacman.x &&this.y < pacman.y + size &&this.y + size > pacman.y) {return true;}return false;
}

6. 其他

后续可以添加分数、时间、生命等模块

总结

游戏整体做下来,很过瘾,学到不少东西,而且成就感满满。这也是第一次用canvas写游戏,以后再写小游戏,首先考虑使用canvas。感觉很棒!现在也不知道为啥开始喜欢写一些小游戏之类的东西了,就感觉比平常业务代码更让人上瘾,尤其把一个游戏亲自做完的时候。以后还会慢慢接触更多的小游戏制作,以及小游戏过程中的不断学习。主要的还是得多看看原生的东西,比如js、canvas之类。也还得多刷刷leetcode的题,解题过程的思考对写小游戏还是有很大帮助的。

最后,

学习中娱乐,娱乐中学习。

谢谢观看!

小游戏之欢乐吃豆人canvas重制版相关推荐

  1. 【py小游戏系列】吃豆人,儿时的回忆

    hello大家好,今天我又发现了个有趣的小玩意.我是专写有趣小玩意的老诗. 老规矩,先上效果图 这是一个吃豆人的小游戏.我们8090后这一代人肯定会碰到过.黄点是我们自己,红点就是怪物们.这是最原始版 ...

  2. Dev-C++5.11游戏创作之吃豆人小游戏(转载)

    Hi!大家好,我是你们的编程小王子!今天为大家转载了一个小游戏, 蒟蒻一枚https://blog.csdn.net/yueyuedog原创 代码我不过多解释,还是比较"简单"的 ...

  3. 吃豆人游戏-第12届蓝桥杯Scratch选拔赛真题精选

    [导读]:超平老师计划推出Scratch蓝桥杯真题解析100讲,这是超平老师解读Scratch蓝桥真题系列的第79讲. 蓝桥杯选拔赛每一届都要举行4~5次,和省赛.国赛相比,题目要简单不少,再加上篇幅 ...

  4. c语言吃豆人游戏怎么理解,python 实现简单的吃豆人游戏

    效果展示: 程序简介 1.使用pygame模组 2.在material目录下有一些素材 3.吃豆人的游戏主体 4.吃豆人怪物的AI(未使用深度学习) 主要代码 main.py import pygam ...

  5. 玩一玩Google涂鸦中的《吃豆人》

    2010年5月为纪念街机游戏<吃豆人>诞生30周年,Google和南梦宫合作创作了第一个交互式涂鸦, Google上线这款涂鸦后, 广受好评, 涂鸦在Google主页展示48小时后暂时下线 ...

  6. 活体机器人学会生孩子:AI进化算法加持变身吃豆人,已经繁殖到「曾孙」

    贾浩楠 发自 凹非寺 量子位 报道 | 公众号 QbitAI AI帮活体机器人生出了孩子! 完全由活体细胞组成.有结构.可编程.能移动的Xenobots,今年又进化出了新的能力. 自我复制繁衍. 去年 ...

  7. 【历史上的今天】5 月 22 日:Windows 3.0 发布;虚幻引擎诞生;《吃豆人》问世

    整理 | 王启隆 透过「历史上的今天」,从过去看未来,从现在亦可以改变未来. 今天是 2022 年 5 月 22 日,在 1994 年的今天,知名中文论坛曙光 BBS 站开通.1994 年 4 月 2 ...

  8. 整活~使用webAI做一个网页AR吃豆人小游戏

    一个好习惯,先给结论 使用网页端深度学习框架识别人脸,做一个AR吃豆人小游戏.吃豆人会随着人脸在镜头内的移动而移动,吃完全部豆子即为获胜. 在线体验地址:点我预览 代码地址:点我github 本文首发 ...

  9. 收藏网页版小游戏:蜘蛛纸牌、扫雷、水果忍者、打地鼠、吃豆人

    学习之余当然是摸鱼了,这里分享几个不用下载直接在线玩耍的游戏.有蜘蛛纸牌网页版在线玩.在线扫雷小游戏.在线玩的水果忍者.吃豆人.打地鼠.3D模仿. 下面我将一个个列出来.欢迎体验收藏! 蜘蛛纸牌:这是 ...

最新文章

  1. 词云图可视化python_python 可视化 词云图
  2. 图像拼接 SIFT资料合集
  3. ACM中Java输入输出
  4. 解决 Visual Studio 2019 无法打开wpf设计器问题
  5. 2021 “AI Earth”人工智能创新挑战赛 AI助力精准气象和海洋预测
  6. 一文读懂领域迁移与领域适应的常见方法
  7. percona-toolkit 之 【pt-table-checksum】、【pt-table-sync】说明
  8. 省选+NOI 第四部分 图论
  9. deepfake ai智能换脸_AI 换脸、声音篡改等,明确写入新版民法典!
  10. 外呼机器人起名_电销外呼机器人如此受欢迎,今天终于知道原因了
  11. debian英文环境中中文输入
  12. 记录一下unity 加载外部视频
  13. C语言编程——输入某年某月某日,判断这一天是这一年的第几天?
  14. Google浏览器常用设置
  15. 软件测试的环境部署怎么做?
  16. shopify 与国内第三方建站服务平台的比较(店匠、shopline、shopyy、ueeshop)
  17. 行人检测/人体检测综述
  18. python3 onvif协议 摄像头控制
  19. Oracle DBA日常工作手册
  20. EAS小贷系统(财务业务一体化)

热门文章

  1. 读《人人都是产品经理》
  2. C语言UNIX时间戳4字节转北京时间
  3. 2021-11-06 编程打印空心菱形
  4. 【Docker】虚悬镜像(Dangling Image)介绍和处理方法
  5. ISP——BLC(Black Level Correction)
  6. 好用的一款文档转换链接(在线免费)
  7. AI智能写作破解:机器人写手的三大秘密
  8. 蓝牙自动配对警惕PIN码漏洞攻击
  9. 台式电脑用网线可以上网,为什么把网线插到笔记本电脑上就连不上网的问题的解决
  10. xp虚拟服务器设置,如何设置虚拟内存 winxp、win2003最正确的设置虚拟内存方法