JavaScript 从零开始实现一个塔防游戏 - 02. 游戏画布类与游戏状态类的基础结构

923 阅读6分钟

前言

在上篇文章里,我们提到了这个游戏制作的起因以及为这个游戏的实现做了准备工作,那么本篇文章将会实现游戏画布类与游戏状态类的基础结构。

目录

  1. JavaScript 从零开始实现一个塔防游戏 - 01.游戏预览与准备工作
  2. JavaScript 从零开始实现一个塔防游戏 - 02. 游戏画布类与游戏状态类的基础结构
  3. JavaScript 从零开始实现一个塔防游戏 - 03. 渲染静态游戏面板,道具类的基础结构与坐标/链表类

一. 游戏画布类

首先我们来回顾一下我们上篇文章的目录结构

其结构如下:


  • css
    • style.css (样式文件)
  • images
    • xxx.svg (图片文件)
  • js
    • Class
      • Bullet.js (子弹类)
      • Canvas.js (画布类)
      • Monster.js (怪物类)
      • Pos.js (坐标类/链表类)
      • State.js (游戏状态类)
      • Tool.js (道具类)
      • Event.js (事件绑定类)
    • Data
      • Config.json (游戏配置文件)
    • app.js (游戏辅助方法以及游戏运行所在文件)
  • index.html (项目入口)

其中我们可以看到 Canvas.jsState.js 是实现 画布类游戏状态类 的文件。

所以我们先从 画布类 着手吧。

1. 进入 Canvas.js 创建一个 Canvas 类

const scale = 30;// 这个常量是为每个单元格的大小,后续会经常用到

class Canvas {
    constructor() {
        // 获取已有的 Canvas Dom 对象
        let canvas = document.querySelector('canvas');

        this.canvas = canvas; // 赋值给其本身,后续可能会使用到。
        this.cx = canvas.getContext('2d'); // 获取画布上下文。

        this.padding = 20; // 内间距 为 20px
        this.siderbarX = 0; // 侧边栏菜单与道具框的偏移量
    }
}

在做好了一个 Canvas 类的基础后,我们先来实现几个 画布类 应有的方法。

2. 绘制方法

(1). 绘制游戏单元格

该方法主要用于绘制游戏单元格。

/**
* 绘制单元格
* @param x {Number} 单元格的 x 坐标
* @param y {Number} 单元格的 y 坐标
* @param w {Number} 单元格的宽度
* @param y {Number} 单元格的高度
* @param borderColor {String} 单元格的边框颜色
* @param bgColor {String} 单元格的背景颜色
* @param strokeW {Number} 单元格的边框宽度
* @return void
*/
renderCell(x, y, w, h, borderColor = '#ddd', bgColor = '#fff', strokeW = 1) {
    this.cx.beginPath();
    this.cx.globalAlpha = 1;
    this.cx.fillStyle = bgColor;
    this.cx.strokeStyle = borderColor;
    this.cx.strokeWidth = `${strokeW}px`;
    this.cx.rect(x, y, w, h);
    this.cx.stroke();
    this.cx.fill();
    this.cx.closePath();
}

(2). 绘制文字方法

该方法主要用于绘制提示以及展示当前游戏数据(如:分数,金币,血量,关卡,时间等)。

/**
* 绘制文字
* @param txt {String} 所要绘制的文本内容
* @param x {Number} 所要绘制的文本内容的 x 坐标
* @param y {Number} 所要绘制的文本内容的 y 坐标
* @param size {Number} 所要绘制的文本内容的字体大小
* @param color {String} 所要绘制的文本内容的字体颜色
* @return void
*/
renderFont(txt, x, y, size = 14, color = '#181818') {
    this.cx.beginPath();
    this.cx.globalAlpha = 1;
    this.cx.font = `${size}px Arial`;
    this.cx.fillStyle = color;
    this.cx.fillText(txt, x, y);
    this.cx.fill();
    this.cx.closePath();
}

(3). 绘制圆方法

该方法主要用于绘制怪物,子弹等。

/**
* 绘制圆
* @param x {Number} 所要绘制的圆形 x 坐标
* @param y {Number} 所要绘制的圆形 y 坐标
* @param radius {Number} 所要绘制的圆形的半径
* @param color {String} 所要绘制的圆形的颜色
* @param alpha {Number} 所要绘制的圆形的透明度 最高为1,最低为0
* @param strokeWidth {Number} 所要绘制的圆形的边框宽度
* @param strokeColor {String} 所要绘制的圆形的边框颜色
* @return void
*/
renderArc(x, y, radius, color, alpha, strokeWidth = 0, strokeColor = '#000') {
    this.cx.beginPath();
    this.cx.fillStyle = color;
    this.cx.strokeStyle = strokeColor;
    this.cx.strokeWidth = `${strokeWidth}px`;
    this.cx.globalAlpha = alpha;
    this.cx.arc(x, y, radius, 0, Math.PI * 2);
    this.cx.fill();
    this.cx.stroke();
    this.cx.closePath();
}

(4). 清除画布内容方法

该方法主要用于清除上一帧所绘制的 canvas 内容。

/**
* 清除画布
* @return void
*/
clearCanvas() {
    this.cx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}

3. 画布更新方法

该方法主要用于更新 canvas 内容。

/**
* 画布更新方法
* @param state {State} 游戏状态类
* @return void
*/
update(state) {
    this.clearCanvas();
    
    // 后续再填入
}

那么至此,我们的 画布类 最基础的结构就已经完成了,后续我们会根据需要再来往 画布类 里添加或更改方法/内容。

二. 游戏状态类

1. 分析

在动手实现游戏状态类之前,我们先要根据下面这个图,来判断有哪些游戏数据。

image.png

首先我们可以看到在该游戏截图里,得知以下信息。

  1. 拥有 血量,得分,金币,时间,关卡等游戏数据。
  2. 拥有 怪物,道具,子弹等数据。
  3. 拥有 提示类数据。
  4. 拥有 地图单元格数据。

在以上数据里,我们可以先思考一下哪些数据是实时变化的数据,哪些不是实时变化的数据。

...

那么很显然,除去 地图单元格的数据外,其他数据多多少少会发生一定的改变。

其中,提示数据是会有个逐渐向上的一个动画,并且在一定时间后自动消失。

所以我们可以先在 state.js 里 创造一个 提示类

// 由于后续我们可能要根据某些条件过滤提示数据,所以我们先为 Set 类添加一个 filter 方法
Set.prototype.filter = function(callback) {
    let arr = [];
    
    /*
     这里我们使用 短路&的原因是,如果前面条件成立,那么我们就将当前的值存入,
     若不成立,则不执行存入操作。
    */
    this.forEach(item => (callback(item)) && arr.push(item));
    
    return arr;
}

class Tips {
    constructor() {
        this.tips = new Set();
    }
}

在创造完这个 提示类 后,我们需要为其实现两个方法,一个是 添加提示数据方法 ,另一个是 更新提示数据值方法

代码如下。

/**
* 添加提示数据
* @param obj {Object} 所要添加的提示数据
* @return void
*/
add(obj) {
    obj.timer = 0;
    this.tips.add(obj);
}

/**
* 更新提示数据方法
* @return void
*/
update() {
    this.tips.forEach(tip => {
        tip.y -= .5; // 更改提示数据的 y 轴数据
        tip.x += .03; // 更改提示数据的 x 轴数据

        tip.timer++;
        // 如果提示数据的时间已经过去了两秒,则删除该提示数据
        if (tip.timer % 120 === 0) this.tips.delete(tip);
    });
}

待到我们把提示数据类实现完毕后,我们就可以先来实现一下 数据状态类 了。

2. 创建 State 类

class State {
    constructor(configData, status, ...images) {
        /* 
        考虑到后续会有多个道具图片传入进来,
        所以我们选用扩展运算符来将从第二个参数后的参数置入 images 里
        */
        this.actors = new Set(); // 动态元素

        this.map = configData.data; // 地图数据
        this.tips = new Tips; // 提示类

        // 道具箱
        this.toolsBox = [
            {
                image: images[0], 
                x: 0, 
                offsetX: 2, 
                y: 0, 
                active: false, 
                radius: 3.5, 
                need_money: 50 
            }
        ];

        // 游戏面板数据
        this.panel = {
            hp: { title: '血量', value: 100 },
            score: { title: '得分', value: 0 },
            money: { title: '金币', value: 200 },
            time: { title: '时间', value: 0, millSecond: 0 },
            wave: { title: '关卡', value: 1 },
        };

        // 状态
        this.status = status;
        
        // 后续会再补充
    }
}

在实现了游戏状态类的基本结构后,我们还需要为其实现一个更新方法和其他两个辅助方法。

其主要负责充当游戏计时器,提示数据与动态元素更新的作用。

以及判定如果血量少于0,结束游戏。

/**
* 游戏状态数据更新方法
* @return void
*/
update() {
    this.timeAdd(); // 这里我们将计时方法单独拿出来写
    this.tips.update();
    this.actors.forEach(actor => actor.update(this));

    if (this.panel.hp.value <= 0) this.status = 'over';
}

/**
* 游戏计时方法
* @return void
*/
timeAdd() {
    this.panel.time.millSecond = (this.panel.time.millSecond + 1) % 60;
    if (this.panel.time.millSecond === 0) this.updatePanelVal('time', 1);
    
    // 后续将会往里面补充
}

/**
* 更新游戏面板数据方法
* @param key {String} 键
* @param val {Number} 值
* @return void
*/
updatePanelVal(key, val) {
    this.panel[key].value += val;
}

那么至此,我们的游戏画布类与游戏状态类的基础结构就实现了。

下篇,我们将会选择绘制一个游戏界面。

(最后有个小小的请求,请问朋友是否可以赏我一个小小的赞呢 😜,您的点赞/收藏将会是我最大的动力~)