小游戏之欢乐吃豆人

前言

今天给大家带来的游戏还是一款经典小游戏《吃豆人》,之前不知道在哪看到的这个游戏,就想着上班闲着时候摸摸鱼,看看能不能写出来。Pac-Man最早的艺名叫Pakkuman,源于“パクパク食べる”的发音paku-paku taberu,paku-paku表示嘴巴一张一合的动作和声音,形象描绘了“我吃,故我在”的生活态度,也希望大家每个人都是PacMan,能把生活中所有烦恼和麻烦都一起吃光光。

废话不多说,直接看代码。

一、游戏布局

游戏主旨就是在规定的地图内,布满金币和墙壁,玩家需要操控PacMan来进行上下左右移动,吃金币,击败怪物,来获取分数,进行通关。

关于布局里面一些细节,下面我都会一一说明

image.png

看到画面中右侧部分是游戏相关的一些说明,这里只说一点,那就是非常好看的一款字体,
可惜这款字体只能用在字母和数字上。

font-family: comic sans MS;
1. 绘制Pac-Man

关于用css画游戏的主角,我是用了一个div + ::before + @keyframes做成的效果。头部是一个圆,嘴巴就是一个伪元素用clip-path画成等腰三角形,然后利用动画改变伪元素的高度即可。运行出来就可以嘴巴一张一合了。

.spirit {
    width: 24px;
    height: 24px;
    background-color: rgb(255,165,0);
    border-radius: 50%;
    position: absolute;
    transition-property: left, top;
    transition-timing-function: linear;
    z-index: 2;
}
.spirit::before {
    content: "";
    position: absolute;
    width: 80%;
    height: 90%;
    right: -1px;
    top: 50%;
    background-color: rgb(5, 31, 1);
    transform: translateY(-50%);
    clip-path: polygon(100% 0, 100% 100%, 20% 50%);
    animation: zmlAniMouth .5s infinite linear;
}
@keyframes zmlAniMouth {
    0% { height: 100%; }
    50% { height: 0%; }
    100% { height: 100%; }
}
2. 绘制地图map

(1)首先地图我是用一个二维数组生成。
其中 0=墙 1=路 2=金币 3=吃豆人 4=怪物 5=大金币
别问为什么2和5都是金币,为啥要离那么远,因为大金币是后来才加的,懒得返回去改了。

image.png

保存运行…!!!有点丑…接下来用js对地图进行优化处理

image.png

我们需要去除砖块之间多余的边框,最起码看起来要像一堵承重墙。

// 首先我们需要获取所有的砖块
// 还要获取所有的砖块元素
const wallList = []; // 获取砖块
const trList = document.querySelectorAll('.tr'); // 获取所有tr元素
for(let i = 0; i < this.list.length; i++) {
        for(let j = 0; j < this.list[0].length; j++) {
                if(this.list[i][j] === 0) {
                        wallList.push([i, j])
                }
        }
}

// 遍历所有墙
for(let i = 0; i < wallList.length; i++) {
        let [x, y] = wallList[i];
        if(this.list[x][y] === 0) {
                // 把墙壁砖块与砖块之间的间隙去除
                // 就是判断每块砖的上下左右有没有其他的砖了,如果有,则去掉对应的边框
                if(x - 1 >= 0 && this.list[x - 1][y] === 0) {
                        trList[x].childNodes[y].childNodes[0].style.borderTop = 0;
                }
                if(x + 1 <= this.row - 1 && this.list[x + 1][y] === 0) {
                        trList[x].childNodes[y].childNodes[0].style.borderBottom = 0;
                }
                if(y - 1 >= 0 && this.list[x][y - 1] === 0) {
                        trList[x].childNodes[y].childNodes[0].style.borderLeft = 0;
                }
                if(y + 1 <= this.col - 1 && this.list[x][y + 1] === 0) {
                        trList[x].childNodes[y].childNodes[0].style.borderRight = 0;
                }
                // 把砖块的角磨平
                // 用css圆角属性,我们只要判断每块砖的四角,每一个角的相邻两个地方有没有砖,如果两个地方都没有,则这个角需要变圆滑。
                if(x-1 >= 0 && y-1 >= 0 && this.list[x-1][y] !== 0 && this.list[x][y-1] !== 0) {
                        trList[x].childNodes[y].childNodes[0].style.borderTopLeftRadius = '10px';
                }
                if(x-1 >= 0 && y+1 <= this.col-1  && this.list[x-1][y] !== 0 && this.list[x][y+1] !== 0) {
                        trList[x].childNodes[y].childNodes[0].style.borderTopRightRadius = '10px';
                }
                if(x+1 <= this.row-1 && y-1 >= 0 && this.list[x+1][y] !== 0 && this.list[x][y-1] !== 0) {
                        trList[x].childNodes[y].childNodes[0].style.borderBottomLeftRadius = '10px';
                }
                if(x+1 <= this.row-1 && y+1 <= this.col-1 && this.list[x+1][y] !== 0 && this.list[x][y+1] !== 0) {
                        trList[x].childNodes[y].childNodes[0].style.borderBottomRightRadius = '10px';
                }
        }
}

再次保存运行…完美!

image.png

3.绘制金币和幽灵

这个就不详细说明了,金币直接div渲染。闪光金币,利用动画+阴影。幽灵直接用图片代替。

二、游戏实现

1. 基本参数
allLevel: [level_1],		// 全部关卡
list: [], 			// 0=墙 1=路 2=金币 3=吃豆人 4=怪物 5=大金币
list_detail: [],		// 存放每个点的详细数据
row: 30,			// 地图默认30行
col: 21,			// 地图默认20列
location: [1, 1],	        // 吃豆人的坐标
dir: 'right',			// 吃豆人的方向
left: 30,			// 吃豆人左距离
top: 30,			// 吃豆人上距离
speed: 100,			// 速度
score: 0,			// 分数	金币+1  大金币+5  怪物+10
timer: null,			// 吃豆人定时器
isStart: false,			// 是否开始
isOver: false,			// 是否结束
isWin: null,			// 是否 赢了
monster: [			// 怪物数组
    { location: [17, 8], left: 0, top: 0, dir: 'right', show: true },
    { location: [17, 9], left: 0, top: 0, dir: 'up', show: true },
    { location: [17, 10], left: 0, top: 0, dir: 'up', show: true },
    { location: [17, 11], left: 0, top: 0, dir: 'left', show: true },
],
timer_monster: null,		// 怪物定时器
speed_monster: 300,		// 怪物速度
isWeak: false,			// 弱化怪物
weakTime: 5,			// 弱化时间
timer_weak: null,		// 弱化定时器
2. 游戏初始化

首先我们需要生成地图以及我们需要地图中每块的详细数据,比如每块左边、上边的距离。
后面我们需要根据left和top值进行吃豆人与怪物的定位。
默认每块的长宽是30

createMap() {
    const level1 = this.allLevel[0];
    const arr = [];
    for(let i = 0; i < level1.length; i++) {
            arr[i] = [];
            for(let j = 0; j < level1[0].length; j++) {
                    arr[i].push({
                            left: j * 30,
                            top: i * 30
                    })
            }
    }
    this.list_detail = JSON.parse(JSON.stringify(arr));
    this.list = level1;
}

然后对地图进行打扮一番,上面已说过。

3. 监听键盘事件

这个地方我们按上下左右的时候,需要判断我们按得键导致移动的点,有没有墙,如果有墙,则改变方向无效

onKeyBoard() {
        document.addEventListener('keydown', (e) => {
                // 空格键开始
                if(e.keyCode == 32 && !this.isStart) {
                        this.isStart = true;
                        this.spiritMove();  // 吃豆人移动
                        this.monsterMove(); // 幽灵移动
                }
                // 开始了 未结束,这时候我们可以操控
                if(this.isStart && !this.isOver && [37,38,39,40].includes(e.keyCode)) {
                        let [x, y] = this.location;
                        let nexDir = '';
                        switch (e.keyCode){
                                case 37: 
                                        nexDir = 'left';
                                        y -= 1;
                                        break;
                                case 38:
                                        nexDir = 'up';
                                        x -= 1;
                                        break;
                                case 39:
                                        nexDir = 'right';
                                        y+=1;
                                        break;
                                case 40:
                                        nexDir = 'down';
                                        x+=1; 
                                        break;
                        }
                        if(this.list[x][y] == 0) {
                                return;
                        }
                        this.dir = nexDir;
                }
        })
}
4. 吃豆人走路

根据方位来进行location的调整,再根据当前的locaiton进行移动。如果碰到墙,则就会一直处于当前位置,不再进行移动,直到再次改变方位

这里我是用css的transition属性让吃豆人从一个点走到下一个点的,这里的移动时间数一定得跟定时器保持一致,这样才能移动完一格之后,才进行下一格的处理,让吃豆人走起路来更丝滑。

<!-- 精灵 -->
<div 
    class="spirit" 
    :style="{
        'transform': 'rotate('+ spiritRotate +'deg)', 
        'left': left + 'px', 
        'top': top + 'px', 
        'transition-duration': speed + 'ms'
    }"
></div>
spiritMove() {
    if(this.timer) {
            clearInterval(this.timer)
            this.timer = null;
            return;
    }
    this.timer = setInterval(() => {
            let [x, y] = this.location;
            switch (this.dir){
                    case 'left': y--; break;
                    case 'up': x--; break;
                    case 'right': y++; break;
                    case 'down': x++; break;
            }
            if(this.list[x][y] == 0) {
                    return;
            }
            this.location = [x, y];
            this.drawSpirit();
    }, this.speed)
}
5. 渲染吃豆人

每次改变完吃豆人location后,我们才要真正的进行移动操作。
详细请看代码注释

drawSpirit() {
        // 根据当前的吃豆人位置,获取当前的left与top值
        let [x, y] = this.location;
        let targetLeft = this.list_detail[x][y].left + 3; 
        let targetTop = this.list_detail[x][y].top + 3;
        [this.left, this.top] = [targetLeft, targetTop];
        
        // 移动完之后,我们还要判断当前这个点有没有小金币或者大金币
        if(this.isStart) {
                // 吃小金币
                if(this.list[x][y] == 2) {
                        this.score++;
                }
                // 吃大金币
                // 吃完还要让所有的怪兽处于弱化状态,处于弱化状态的怪兽可以被吃
                // 每个大金币弱化时间为自定义五秒,如果你吃的够快,则会重新刷新弱化时间
                if(this.list[x][y] == 5) {
                        this.score += 5
                        clearInterval(this.timer_weak);
                        this.timer_weak = null;
                        this.isWeak = true;
                        this.onWeakTimer()
                }
                // 移动完之后,需要改变一下地图中吃豆人的位置(隐藏状态的吃豆人)
                setTimeout(() => {
                        for(let i = 0; i < this.list.length; i++) {
                                for(let j = 0; j < this.list[0].length; j++) {
                                        if(this.list[i][j] === 3) {
                                                this.list[i][j] = 1;
                                        }
                                }
                        }
                        this.list[x][y] = 3;
                        this.die();
                        this.win();
                        this.$forceUpdate();
                }, this.speed/2); // 这里除2是因为,我想让吃豆人走到格子一半的时候,金币才消失,这样才像吃到嘴里的
        }
}

样式中给它们加个类名就可以了

.monster.isWeak {
	filter: grayscale(1);
}

image.png

6. 弱化定时器
onWeakTimer() {
    let time = 0;
    this.timer_weak = setInterval(() => {
        time++;
        if(time == this.weakTime) {
            clearInterval(this.timer_weak);
            this.isWeak = false;
        }
    }, 1000);
}
7. 幽灵走路

我们首先会给幽灵赋予方向感,然后让它自己顺着自己的方向进行移动。一个方向移动到底,碰到墙壁,则会随机取其他方位,再次进行移动。

monsterMove() {
    this.timer_monster = setInterval(() => {
        // 循环遍历所有幽灵
        for (let i = 0; i < this.monster.length; i++) {
            // 已经被吃的就不处理了
            if(this.monster[i].show) {
                    let [x, y] = this.monster[i].location;
                    let dir = this.monster[i].dir;
                    // 首先需要拿到下一步的坐标,判断能不能走
                    switch (dir) {
                            case 'left': y--; break;
                            case 'up': x--; break;
                            case 'right': y++; break;
                            case 'down': x++; break;
                    }
                    // 判断下一步移动的地方是否有墙,
                    // 如果有墙,则随机从其他方位选一个继续移动
                    if(this.list[x][y] == 0) {
                            // 因为上面加过或者减过了,因为有墙不能走,则需要再加回来或者减回去,重新取其他方位走
                            switch (dir) {
                                    case 'left': y++; break;
                                    case 'up': x++; break;
                                    case 'right': y--; break;
                                    case 'down': x--; break;
                            }
                            // 判断其他范围有没有可以走的地,也就是判断上下左右有没有不是墙的
                            let arr = [];
                            if(x - 1 >= 0 && (this.list[x - 1][y] === 1 || this.list[x - 1][y] === 2)) {
                                    arr.push({dir: 'up', location: [x-1, y]})
                            }
                            if(x + 1 <= this.row - 1 && (this.list[x + 1][y] === 1 || this.list[x + 1][y] === 2)) {
                                    arr.push({dir: 'down', location: [x+1, y]})
                            }
                            if(y - 1 >= 0 && (this.list[x][y - 1] === 1 || this.list[x][y - 1] === 2)) {
                                    arr.push({dir: 'left', location: [x, y-1]})
                            }
                            if(y + 1 <= this.col - 1 && (this.list[x][y + 1] === 1 || this.list[x][y + 1] === 2)) {
                                    arr.push({dir: 'right', location: [x, y+1]})
                            }
                            // 随机取一个方位进行移动
                            let index = Math.floor(Math.random() * arr.length);
                            this.monster[i].dir = arr[index].dir;
                            this.monster[i].location = arr[index].location;
                            this.drawMonster();
                            return;
                    }
                    // 如果没有墙,则会一直顺着一个方位走下去
                    this.monster[i].location = [x, y];
                    this.drawMonster();
            }
        }
    }, this.speed_monster);
}

image.png

7. 渲染幽灵

我们处理的过程中,不但要判断吃豆人能不能撞到幽灵,还要判断幽灵会不会撞到吃豆人。也就是说我站那不动,幽灵撞过来,我也会挂掉。

drawMonster() {
    for (let i = 0; i < this.monster.length; i++) {
        if(this.monster[i].show) {
            let [x, y] = this.monster[i].location;
            this.monster[i].left = this.list_detail[x][y].left + 3;
            this.monster[i].top = this.list_detail[x][y].top + 3;
        }
    }
    // 每次渲染完,就要判断有没有撞到吃豆人
    this.die();
    this.$forceUpdate();
}
8. 判断会不会挂掉

判断吃豆人的坐标是否与幽灵坐标重叠。

如果重叠了,还要判断当前幽灵是不是弱化状态的,如果是,就会变成“我”吃幽灵了

如果我们把所有幽灵都吃了,那就会直接获胜,直接获取所有金币奖励

die() {
    // 吃豆人的坐标
    const [xA, yA] = this.location;
    // 遍历所有活的幽灵
    for (let i = 0; i < this.monster.length; i++) {
            if(this.monster[i].show) {
                    let [x, y] = this.monster[i].location;
                    // 幽灵没弱化,我直接挂掉
                    if(x == xA && y == yA && !this.isWeak) {
                            clearInterval(this.timer);
                            clearInterval(this.timer_monster);
                            this.isOver = true;
                            this.isWin = false;
                            break;
                    }
                    // 幽灵弱化了
                    if(x == xA && y == yA && this.isWeak) {
                            // 直接吃掉
                            this.monster[i].show = false;
                            this.score += 10;
                            // 每次吃怪物都要判断,是否把怪物吃完了,如果吃完为了,则直接获胜
                            const arr = this.monster.filter(item => item.show);
                            if(arr.length === 0) {
                                    let arrBeansSmall = 0; // 剩余所有小金币分数
                                    let arrBeansBig = 0;   // 剩余素有大金币分数
                                    for(let i = 0; i < this.list.length; i++) {
                                            for(let j = 0; j < this.list[0].length; j++) {
                                                    if(this.list[i][j] === 2) {
                                                            this.list[i][j] = 1;
                                                            arrBeansBig++;
                                                    }
                                                    if(this.list[i][j] === 5) {
                                                            this.list[i][j] = 1;
                                                            arrBeansBig += 5;
                                                    }
                                            }
                                    }
                                    this.score = this.score*1 + arrBeansSmall*1 + arrBeansBig*1;
                                    this.win();
                            }
                    }
            }
    }
    this.$forceUpdate();
}
9. 判断有没有赢

只要判断当前场上还有没有大金币或者小金币了

win() {
    let arr = [];
    for(let i = 0; i < this.list.length; i++) {
            for(let j = 0; j < this.list[0].length; j++) {
                    if(this.list[i][j] === 2 || this.list[i][j] === 5) {
                            arr.push([i, j]);
                    }
            }
    }
    if(arr.length === 0) {
            clearInterval(this.timer);
            clearInterval(this.timer_monster);
            this.isOver = true;
            this.isWin = true;
            return;
    }
    this.$forceUpdate();
}

image.png

总结

游戏做出来还是比较喜欢的,用css+js做的确有些地方处理是不太好的。
准备这两天用canvas重新做一版出来,到时候利用碰撞检测处理这些效果,让游戏更丝滑,更具有游戏色彩。

谢谢观看!

完整代码:
zml-game-pacman: 《吃豆人》 (gitee.com)

喜欢h5游戏的可以看我其他文章,还希望看到的大兄弟可以点个赞,谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张_大_炮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值