🔥如何自己动手做一个「canvas游戏」?我手把手教你~

2,269 阅读5分钟

我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

生活不止眼前的苟且,还有诗和远方

掘友们,大家好我是前端奶爸,入行5年的前端小学生🥜~
工作八小时摸鱼四小时,喜欢跑步但不是为了让自己更瘦,而是为了让自己活得更久~
活到九十九,卷到九十九~

前言

很久没更文了,最近一直因为各种原因,没特别充足的时间写文章,借着平台有游戏的活动,跟大家分享一个以前自己制作的canvas游戏,代码也是未压缩的,纯js代码,有需要或者想学习canvas入门动画的,可以参考下~

闲言少叙,咱们正题走起~

游戏地址

体验地址:https://dxh-vip.github.io/mountain-climbing-game/

仓库地址::https://github.com/dxh-vip/mountain-climbing-game

目前只支持手机端体验,代码也是纯js,无压缩,欢迎喜欢的朋友们start和fork~

目前只支持手机端体验~~

目前只支持手机端体验~~

目前只支持手机端体验~~

重要的事情说三遍

游戏规则

游戏的想法来源于慕士塔格峰,很多电影都讲到了人类征服山峰的故事,这个游戏也因此而来,使用canvas模拟人物攀登高峰的场景。

image.png

简单来说就是在规定的时间攀爬登山的小游戏,攀登的过程中会有障碍和滚落的雪球,玩家需要通过控制左右移动按钮来操作闪避,攀爬的过程中会有相应的补给出现,可以给自己的补给罐充能,提升自己的续航能力~

主要实现步骤解析

思路

游戏整体可以分为几大块:场景 人物 障碍补给 动画 工具方法

场景

首先说下场景中的背景,背景使用的是一张长度为8000像素高度的循环背景雪山图,通过设置初始运动速度,初始宽高能操作实现,具体初始代码如下:

// 背景
function background() {
    this.x = 0; //初始X轴位置
    this.y = 0; //初始Y轴位置
    this.w = canvas.width; //宽度
    this.h = canvas.height; //高度
    this.dx = 0; //图片在雪碧图的X轴位置
    this.dy = 7546; //图片在雪碧图的Y轴位置
    this.mh = 7546; // 向上爬行高度
    this.speed = 1; //运动基础速度
}

接下来是场景左侧的血瓶,以及右侧的登山标尺,是通过布局结合背景图的方式进行实现的,通过开启运动后根据实际的计算值进行动态的赋值操作。

// 血量展示
function bloodVolume(blood) {
    document.querySelector("#bloodVolume").style.height = (blood / 100) * 140 + "px"; //实时记录血量变化
}
// 距离展示
function moveDistance(y) {
    document.querySelector("#moveDistance").style.bottom = (y / 100) * 140 + 10 + "px"; //实时记录血量变化
}

最后就是悬浮在背景上方的左右移动按钮,左右按钮通过定位的方式进行位置确定,结合事件进行小人在整个场景中的左右移动操作。

人物

小人的创建和背景类似,设置了一些初始值,比如说初始位置,初始血量等等。

// 小人
function person() {
    this.x = canvas.width / 2 - 135 / 2; // 小人初始X轴位置
    this.y = canvas.height - 120; // 小人初始Y轴位置
    this.dx = 0;
    this.dy = 740;
    this.w = 135;
    this.h = 159;
    this.blood = 100; //血量
    this.role = "person";
    this.state = 1;
    this.stateNum = 0;
}

障碍补给

障碍分为两类,一种是爬升过程中随机出现的冰山,如果碰到会有血量减少,还有一种是从上方随机出现的雪快,被雪块砸到也会有同样的掉血操作。

// 雪球
function snowBall() {
    this.x = parseInt(Math.random() * 460 + 50);
    this.y = -41.5;
    this.dx = 157;
    this.dy = 0;
    this.w = 81;
    this.h = 83;
    this.speed = 3;
    this.effect = -15;
    this.role = "snowBall";
}

// 雪块
function snowBlock() {
    this.x = parseInt(Math.random() * 460 + 50);
    this.y = -31.5;
    this.dx = 70;
    this.dy = 0;
    this.w = 81;
    this.h = 81;
    this.speed = 1;
    this.effect = -8;
    this.role = "snowBlock";
}

补给是会在爬升过程中随机刷新到场景的位置中,碰到后会有回复生命值的加成。

// 药瓶
function bloodBottle() {
    this.x = parseInt(Math.random() * 460 + 50);
    this.y = -40.5;
    this.dx = 0;
    this.dy = 0;
    this.w = 55;
    this.h = 81;
    this.effect = 10;
    this.speed = 1;
    this.role = "bloodBottle";
}

动画

游戏最重要的其实就是动画,为了实现类似的效果,想了很多的办法,也做了很多尝试和优化,目前的整体感觉还是不完美,还得继续优化。

整个动画的绘制基本都在下方函数中实现:

function drawImages() {
    canvas.ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawImages.timestamp += 10; //刷新掉落物品的时间
    if (all != "") {
        invasion(drawImages.timestamp);
        all.forEach(function (item) {
            setTimeout(function () {
                if (item.y > windowHeight && all != "") all.remove(item);
            }, 1000);
            canvas.ctx.beginPath();
            canvas.ctx.globalAlpha = 1;
            var total = item.__proto__.constructor.name;
            if (total == "snowBall") {
                // 判断是否为雪球
                item.y += 1.5 * item.speed;
            } else if (total == "snowBlock") {
                // 判断是否为雪球
                item.y += item.speed;
            } else if (total == "bloodBottle") {
                //判断是否为血药
                item.y += item.speed;
            } else if (total == "person") {
                // 判断是否为小人
                item.x = heroX;
            }
            if (total == "background") {
                item.dy -= item.speed;
                // 距离展示
                moveDistance(item);
                function moveDistance(item) {
                    document.querySelector(".moveNum").innerHTML = parseInt(item.mh - item.dy) + "m";
                    document.querySelector("#moveDistance").style.bottom = ((item.mh - item.dy) / item.mh) * 147 + 10 + "px"; //实时记录血量变化
                }
                if (item.dy <= 0) {
                    isOverFlag = true;
                    isOver(isOverFlag);
                }
                canvas.ctx.drawImage(raiden_bg, item.dx, item.dy, item.w, item.h, item.x, item.y, item.w, item.h);
                } else if (total == "person") {
                    item.stateNum += 0.1;
                    item.blood -= 0.03;
                    bloodVolume(item.blood); //实时记录血量变化
                    if (item.blood <= 0) {
                        isOverFlag = false;
                        isOver(isOverFlag);
                    }
                    switch (parseInt(item.state)) {
                        case 0: //向左爬动
                            personAnimate(item, 265);
                            break;
                        case 1: //未移动
                            personAnimate(item, 430);
                            break;
                        case 2: //像右移动
                            personAnimate(item, 100);
                            break;
                    }
                } else {
                    canvas.ctx.drawImage(raiden_props, item.dx, item.dy, item.w, item.h, item.x, item.y, item.w / 2, item.h / 2);
                }
            });
        }
        try {
            drawImages.timer = requestAnimationFrame(drawImages);
        } catch (error) {}
        all && all.forEach(function (item) {
        drawImages.first = item;
        all && all.forEach(function (other) {
            drawImages.another = other;
            drawImages.another !== drawImages.first && ishit(drawImages.first, drawImages.another);
        });
    });
}

工具方法

工具方法提供了碰撞检测函数等,就不一一列出了,在仓库都有。

后续逻辑丰富

在游戏进行过程中,还新增了失败弹窗,成功弹窗,再来一次等逻辑交互,因为游戏本身是个单机游戏,没有实现排行榜等功能,后续可以基于微信登录的方式增加排行榜,进行排名比较,还可以根据玩家所得成绩结合视频的形式做一些其他可扩展的交互内容。

总结

整体小游戏能考虑的地方都考虑到了,交互也比较完整,算是比较完整的一个demo,当时场景只是移动端需要,没有做pc端的兼容处理,后续有需要的小伙伴可以继续完善。

整个小游戏的实现,修改过很多次,感觉对自己的代码逻辑,以及一些方法的设计和整体代码的结构和规划都有提升,如果有时间,有兴趣,建议大家都多多尝试尝试~

愿各位掘友们都能到达自己的巅峰~~~