关于我写了一个海底掘金挑战游戏

614 阅读7分钟

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!

先赞后看,养好习惯~

前言

最近好久没更新啦,其实就是最近憋了个大招。我运用jquery和Vue.js仿写了一个掘金的海底掘金挑战游戏,本来不需要话很长时间就能写完的东西,但是因为最近有点事情鸽了一下,然后写这个的时候也碰到了一些问题所以才这么久更新。那么我们直接切入主题,先来介绍一下我完成的功能点吧。

板块介绍

第一眼看上去可能比较懵逼,其实是我不想用图片啦,因为之前写的文章里在线代码里图片报错会导致游戏运行不了所以就纯代码流了,一下色块都对标一个元素

  1. 绿色:游戏玩家
  2. 蓝色:矿石
  3. 黄色:海星
  4. 紫色:宝石
  5. 黑色:石头

image.png

image.png

游戏规则我就直接把掘金本身的部分游戏介绍

image.png

image.png

# 如何实现 这个游戏的难点其实一直都是在放置如何走的板块工具上,看看我这几个app1-3js文件就知道我到底经历了些啥了。

image.png

HTML和CSS如何搭建页面这里肯定就不会放出来解析了,可以直接移步下面的在线代码里细细了解。接下来我会分为以下7大点进行解析:
  1. 初始化Vue
  2. 绘制地图类的封装
  3. 自动随机生成地图
  4. 绘制地图
  5. 工具栏的生成
  6. 工具板块拖拽
  7. 玩家移动和获取物品
  8. 删除所有要执行的板块

初始化Vue

window.app = new Vue({
    el: "#app",
    data: {
        // canvas绘画类
        draw: null,
        // 游戏主体功能类
        game: new Game,
        // 工具板块
        tools: [
            {type: 'top',font: '↑', many: 3},
            {type: 'bottom',font: '↓', many: 3},
            {type: 'left',font: '←', many: 3},
            {type: 'right',font: '→', many: 3},
            {type: 'age', font: '↺', many: 3},
            {type: 'jump', font: '➾', many: 3},
        ],
        // 当前使用的工具板块
        tool: [],
        // 是否开始游戏
        open: false,
        // 当前已经放置要执行的移动板块
        step: [],
        // 获取到的矿石
        ore: 0,
        // 获取到的星星
        star: 0,
        // 当前添加板块的节点
        ele: false,
        // jump的节点保存
        dir: [],
        dirif: false
    },
    mounted(){
        this.draw = new Draw;
        // 地图绘制
        this.game.createMap();
    }
})

绘制地图类的封装

这里就是封装了一个名为Draw的类,获取一下canvas,还有封装60帧的地图更新、绘制开始和绘制结束的函数。

class Draw {
    constructor() {
        this.canvas = document.querySelector('canvas');
        this.ctx = this.canvas.getContext('2d');

        this.run();
    }

    run(){
        if(app.draw){
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        }
        requestAnimationFrame(this.run.bind(this))
    }

    begin(){
        this.ctx.beginPath();
        this.ctx.save();
    }

    close() {
        this.ctx.closePath();
        this.ctx.restore();
    }
}

自动随机生成地图

先封装好一个通用的 Item 类。方便后面一些通用的东西继承使用

class Item {
    constructor(i, j) {
        // 格子的横向做标
        this.i = i;
        // 格子的纵向做标
        this.j = j;
        // 具体像素距离
        this.x = this.i * 65;
        this.y = this.j * 65;
        // 格子的颜色
        this.color = '';
    }

    // 绘制色块
    update(){
        if(this.color === '') return
        app.draw.begin();
        app.draw.ctx.fillStyle = this.color;
        app.draw.ctx.fillRect(this.x, this.y, 65, 65);
        app.draw.close();
    }
}

然后就是地图最初的方块绘制Grid类

class Grid extends Item{
    constructor(i, j) {
        super(i, j);
        // 地图元素
        this.goods = null;
        // 玩家
        this.player = null;
    }
    add(name=''){
        let randomNum = ~~(Math.random() * 90);

        if(name === 'player'){
            this.player = new cont['player'](this.i, this.j);
        }
        if(this.i !== 3 || this.j !== 2){
            if(randomNum <= 30){
                this.goods = null
            }else if(randomNum <= 50){
                this.goods = new cont['wall'](this.i, this.j)
            }else if(randomNum <= 90){
                if(randomNum <= 68){
                    this.goods = new cont['ore'](this.i, this.j)
                }else if(randomNum <= 86){
                    this.goods = new cont['pearl'](this.i, this.j)
                }else{
                    this.goods = new cont['star'](this.i, this.j)
                }
            }
        }

        return this.player || this.goods;
    }
// 获取移动后占有色块可以获得的东西
get(i, j){
    let grid = app.game.get(i, j);
    if(!grid) return;
    let flag = true;
    if(grid.goods instanceof Ore){
        app.ore += grid.goods.ore;
        grid.goods = null;
    }

    if(grid.goods instanceof Pearl){
        app.tools.map((item, i) => {
            if(grid.goods && item.type === grid.goods.dir){
                app.tools[i].many += grid.goods.num;
                grid.goods = null;
                return true;
            }
        })
    }

    if(grid.goods instanceof Star){
        app.star += 1;
        app.game.maps.flat().map(item => {
            if(item.goods instanceof Ore){
                item.goods.ore = item.goods.ore * 2;
            }
        })
        grid.goods = null;
    }

    if(grid.goods instanceof Wall) {
        this.timer.map(item => clearTimeout(item))
        flag = false;
    }
    return flag;
}

    // 绘制网格地图
    update(){
        // 地图元素的渲染
        this.goods && this.goods.update();
        // 玩家渲染
        this.player && this.player.update();

        // 画地图格子
        app.draw.begin();
        app.draw.ctx.strokeStyle = '#A2BDE3';
        app.draw.ctx.strokeRect(this.x, this.y, 65, 65)
        app.draw.close();
    }
}

然后那些墙、矿石、珍珠、星星、玩家都可以通过继承Item类之后,然后通过Grid的add来渲染出来

// 石头墙
class Wall extends Item{
    constructor(i, j) {
        super(i, j);
        this.color = '#555';
    }
}

// 星星
class Star extends Item{
    constructor(i, j) {
        super(i, j);
        this.color = 'yellow';
    }
}

// 矿石
class Ore extends Item{
    constructor(i, j) {
        super(i, j);
        this.color = 'skyblue';
        this.ore = 10 + ~~(Math.random() * 90)
    }

    // 随机矿石的个数,并渲染
    info(){
        app.draw.begin();
        app.draw.ctx.font = 'normal 40px "楷体"';
        app.draw.ctx.fillStyle = "#fff";
        app.draw.ctx.textAlign = 'center';
        app.draw.ctx.fillText(this.ore, this.x + 30, this.y + 50);
        app.draw.close();
    }
}

// 珍珠
class Pearl extends Item{
    constructor(i, j) {
        super(i, j);
        this.color = '#eecfff';
        this.dir = '';
        this.num = 0;

        this.randomDir();
    }

    // 随机珍珠的功能
    randomDir(){
        let data = ['left', 'right', 'top', 'bottom', 'age', 'jump'];
        this.dir = data[~~(Math.random() * 6)]
        this.num = 2 + ~~(Math.random() * 10);
    }

    info(){
        let icon = {left: "←", right: "→", top: "↑", bottom: "↓", age: '↺', jump: '➾'};
        app.draw.begin();
        app.draw.ctx.font = 'normal 20px "楷体"';
        app.draw.ctx.fillStyle = "#fff";
        app.draw.ctx.fillText(icon[this.dir], this.x + 8, this.y + 50);
        app.draw.close();

        app.draw.begin();
        app.draw.ctx.font = 'normal 20px "楷体"';
        app.draw.ctx.fillStyle = "#fff";
        app.draw.ctx.fillText(this.num, this.x + 40, this.y + 50);
        app.draw.close();
    }
}
class Player extends Item{
    constructor(i, j) {
        super(i, j);
        this.color = 'yellowgreen';
    }

    // 移动
    move(data){
        this.timer = [];
        data.map((item, i) => {
             this.timer[i] = setTimeout(() => {
                 if(item === 'left' && this.get(this.i - 1, this.j)){
                         this.i -= 1
                         this.x = this.i * 65
                 }else if(item === 'right' && this.get(this.i + 1, this.j)){
                         this.i += 1
                         this.x = this.i * 65
                 }else if(item === 'top' && this.get(this.i, this.j - 1)){
                         this.j -= 1
                         this.y = this.j * 65
                 }else if(item === 'bottom' && this.get(this.i, this.j + 1)){
                         this.j += 1
                         this.y = this.j * 65
                 }else if(item === 'jumpleft' && this.get(this.i - 2, this.j)){
                     this.i -= 2;
                     this.x = this.i * 65
                 }else if(item === 'jumpright' && this.get(this.i + 2, this.j)){
                     this.i += 2;
                     this.x = this.i * 65
                 }else if(item === 'jumptop' && this.get(this.i, this.j - 2)){
                     this.j -= 2;
                     this.y = this.j * 65
                 }else if(item === 'jumpbottom' && this.get(this.i, this.j + 2)){
                     this.j += 2;
                     this.y = this.j * 65
                 }
            }, 500 * (i + 1))
        })
    }
}

现在还只是封装好了,还没有真正的添加到Grid类里,也就还没添加到地图里,这里我在最后把这些元素都放到了一个叫cont的数组里去方便调用。

let cont = {ore: Ore, wall: Wall, star: Star, pearl: Pearl, player: Player};

然后再在Grid类里封装一个add函数,用于随机添加每个元素,这里面随机也是有说法的。经过我对原版游戏的研究,这里面随机出来的元素的比例大概是:

  1. 空白区域 33.3%
  2. 石头 22.2%
  3. 矿石 20%
  4. 珍珠 20%
  5. 海星 4%
class Grid extends Item{
    constructor(i, j) {
        super(i, j);
        this.goods = null;
        this.player = null;
    }

    // 随机并添加所有的元素
    add(name=''){
        let randomNum = ~~(Math.random() * 90);

        if(name === 'player'){
            this.player = new cont['player'](this.i, this.j);
        }
        if(this.i !== 3 || this.j !== 2){
            if(randomNum <= 30){
                this.goods = null
            }else if(randomNum <= 50){
                this.goods = new cont['wall'](this.i, this.j)
            }else if(randomNum <= 90){
                if(randomNum <= 68){
                    this.goods = new cont['ore'](this.i, this.j)
                }else if(randomNum <= 86){
                    this.goods = new cont['pearl'](this.i, this.j)
                }else{
                    this.goods = new cont['star'](this.i, this.j)
                }
            }
        }

        return this.player || this.goods;
    }

    update(){
        this.goods && this.goods.update();
        this.player && this.player.update();

        app.draw.begin();
        app.draw.ctx.strokeStyle = '#A2BDE3';
        app.draw.ctx.strokeRect(this.x, this.y, 65, 65)
        app.draw.close();
    }
}

绘制地图

这个我也封装成了一个类来动态生成我的地图,里面的数据大概是这样的:

image.png

class Game {
    constructor() {
        // 横向格子数
        this.i = 6;
        // 纵向格子数
        this.j = 50;

        // 保存每个格子
        this.maps = []
        this.player = null;
    }

    // 绘制地图
    createMap(){
        this.maps = new Array(this.j).fill(0).map((item, j) => {
            return new Array(this.i).fill(0).map((item, i) => {
                let grid = new Grid(i, j);
                grid.add();
                return grid;
            })
        })

        let grid = this.get(3,2);
        this.player = grid.add('player');
    }

    // 获取单个格子
    get(i, j){
        return this.maps[j] && this.maps[j][i]
    }
}

工具栏的生成

我的工具栏并非在html中写死的,而是通过Vue里的for循环动态添加的,这样的好处就是方便操作你要使用的当前的工具的数据,也就是初始化Vue的工具板块


// 工具板块
tools: [
    {type: 'top',font: '↑', many: 3},
    {type: 'bottom',font: '↓', many: 3},
    {type: 'left',font: '←', many: 3},
    {type: 'right',font: '→', many: 3},
    {type: 'age', font: '↺', many: 3},
    {type: 'jump', font: '➾', many: 3},
],
tool: [],
<!--html-->
<li v-for="item in tools" @mousedown="tool = item">
    <p>{{item.font}}</p>
    <span>{{item.many}}</span>
</li>

工具板块拖拽

个人认为这是游戏中最难写的地方,真的花了我不少时间去修改代码,甚至重复工具到现在还没研究出来,先放出来我的工具板块页面一整个的Html板块代码

<div class="tool-list" @mouseup="onMouse" @mousemove="onMouse">
    <ul>
        <li v-for="item in tools" @mousedown="tool = item">
            <p>{{item.font}}</p>
            <span>{{item.many}}</span>
        </li>
    </ul>
    <div class="close-tool" @click="ocToolList">
        x
    </div>
    <!--开始游戏-->
    <div class="start" @click="play">
        Start
    </div>
    <!--放置区域的板块-->
    <div class="put">
        <div class="put-top go">GO</div>
    </div>
    <!--跟随鼠标的板块-->
    <div class="put-div">
        <span></span>
    </div>
    <!--删除-->
    <div class="remove" @click="removePut">
        删除
    </div>
</div>

并不多,因为大多数东西都是动态添加,首先我实现的是拖拽功能,我写了一个onMouse函数,用于进行鼠标移动和抬起之后做一些判断,鼠标移动的时候要进行是否移动到了添加执行板块的地方,然后显示一个虚拟的板块,执行updateAdd()和碰撞判断add()函数

image.png

松开之后才真正添加到这个put板块节点里,执行putAdd()函数,并把数据添加到step数组里

image.png

onMouse(ev){
    ({
        mousemove(){
            if(this.tool.many && this.tool.many > 0){
                // 跟随鼠标的浮动板
                $('.put-div').css({
                    display: 'block',
                    left: ev.clientX - $('#app').position().left - ($('.put-div').width() / 2) + "px",
                    top: ev.clientY - $('#app').position().top - ($('.put-div').height() / 2) + "px",
                })

                $('.put-div').addClass(this.tool.type)
                // 添加里面的符号
                $('.put-div>span').html(this.tool.font)
                
                // 判断是否和添加进执行板块的板块直接碰撞了 add是我封装的函数下面有
                if(this.add(ev.clientX - $('#app').position().left, ev.clientY - $('#app').position().top)){
                    this.updateAdd('put')
                }
                else{
                    this.removeOp()
                }
            }
        },
        mouseup(){
            if(this.add(ev.clientX - $('#app').position().left, ev.clientY - $('#app').position().top)){
                this.putAdd()
            }
            $('.put-div').removeClass(this.tool.type)
            this.removeOp()
            this.tool = [];
            $('.put-div').css({
                display: 'none',
            })

            $('.put-div>span').html('')
        }
    })[ev.type].call(this)
}
// 删除虚拟板块
removeOp(){
    $(this.ele).remove();
    this.ele = false;
},
// 更新要执行的虚拟板块状态
updateAdd(){
    this.removeOp();
    this.ele = $(`<div class="put-item ${this.tool.type}"><span>${this.tool.font}</span></div>`)
    if($(`.put>.put-item`).length >= 1){
        $(`.put>.put-item`).eq($(`.put>.put-item`).length - 1).after(this.ele)
    }else{
        $(`.put`).append(this.ele);
    }
    if(this.tool.type === 'jump'){
        let dir = $(`
            <select>
                <option value="left">←</option>
                <option value="right">→</option>
                <option value="top">↑</option>
                <option value="bottom">↓</option>
            </select>`)
        this.dirif && this.dir.pop();
        this.dir.push(dir);
        this.dirif = true;
        $(this.ele).append(dir)
    }
    $(this.ele).css({
        opacity: 0.4
    })
},
// 添加进执行板块
putAdd(){
    $(this.ele).css({opacity: 1});
    this.ele = false;
    this.dirif = false;

    if(this.tool.many !== 0){
        this.tool.many -= 1;
    }
    this.tool.type && this.step.push(this.tool.type);
},
// 判断是否重合添加
add(i, j){
    let l2 = $('.put').position().left
    let r2 = $('.put').position().left+$('.put').width();
    let t2 = $('.put').position().top
    let b2 = $('.put').position().top+$('.put').height();

    if(l2 > (i + 80) || r2 < (i - 80) || t2 > (j + 30) || b2 < (j - 30)){
        return false;
    }else{
        return true;
    }
},
// 开始根据板块移动
play(){
    this.step = this.step.map((item, i) => {
        if(item === 'jump') return 'jump'+$(this.dir[i]).val()
        else return item;
    })
    this.game.player.move(this.step);
    this.step = [];
    this.dir = [];
    $('.put-item').remove();
    this.ocToolList();
},
// 删除放置的东西
removePut(){
    this.tools.map((item, i) => {
        this.step.map(item2 => {
            if(item.type === item2){
                this.tools[i].many += 1;
            }
        })
    })
    this.step = [];
    $('.put-item').remove();
},

玩家移动和获取物品

这里就相对比较简单了,点击start按钮,开始执行play函数,将数据传入Player类里进行判断并移动

// 开始根据板块移动
play(){
    this.step = this.step.map((item, i) => {
        if(item === 'jump') return 'jump'+$(this.dir[i]).val()
        else return item;
    })
    this.game.player.move(this.step);
    this.step = [];
    this.dir = [];
    $('.put-item').remove();
    this.ocToolList();
},

删除所有要执行的板块

这就已经是最后的删除功能啦,添加进put里的板块删除并且把失去的板块数量返还

// 删除放置的东西
removePut(){
    this.tools.map((item, i) => {
        this.step.map(item2 => {
            if(item.type === item2){
                this.tools[i].many += 1;
            }
        })
    })
    this.step = [];
    $('.put-item').remove();
}

在线代码

以上就已经是本期的所有解析啦,希望大家能喜欢!如果大家有什么意见和建议可以在评论区留言,我肯定会看的到的。页面在线代码视图区太小了,建议直接创建个本地文件放进去体验一下QAQ

往期精彩

关于我随手写了个掘金相关的游戏juejin.cn/post/714232…

关于我帮领导的孩子写了一个小游戏参赛这种事juejin.cn/post/714115…

关于我抽不到月饼礼盒于是用代码做了一个(纯代码文本) juejin.cn/post/714047…

关于我仿做了个steam很火的《Helltaker》游戏juejin.cn/post/712149…

如果大家喜欢的话,请为我的文章点点关注点点赞,瑞斯败!jym

3U%H7Y9T8P@KR){A$ZR1$VT.gif