JavaScript 从零开始实现一个塔防游戏 - 03. 渲染静态游戏面板,道具类的基础结构与坐标/链表类

494 阅读5分钟

前言

在上篇文章里,我们实现了 游戏画布类与游戏状态类的基本结构。那么本篇文章里,我们就准备实现道具类的基础结构、坐标/链表类与渲染游戏面板。

目录

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

一. 坐标/链表类

首先我们进入 pos.js 文件里,创建一个 Pos 类。

class Pos {
    constructor(x, y, next = null) {
        this.x = x; // x 轴值
        this.y = y; // y 轴值
        
        this.next = next; // 当此对象作用于链表类的时候,通过该属性来连接下一个节点。
    }
}

其次我们来为其实现几个后面可能会用到的方法

/**
* 更改x y的值
* @param x {Number} 更改的x值
* @param y {Number} 更改的y值
* @return void
*/
set(x, y) {
    this.x = x;
    this.y = y;
}

/**
* 在该对象的属性x与y的值的基础上,乘以一个值,并返回一个新的坐标类
* @param num {Number} 值
* @param bool {Boolean} 判定是否生成一个链表结构
* @return {Pos} 坐标类
*/
factor(num, bool = false) {
    return new Pos(this.x * num, this.y * num, bool ? this.next : null);
}

/**
* 在该对象的属性x与y的值的基础上,加上一个值,并返回一个新的坐标类
* @param num {Number} 值
* @param bool {Boolean} 判定是否生成一个链表结构
* @return {Pos} 坐标类
*/
add(num, bool = false) {
    return new Pos(this.x + num, this.y + num, bool ? this.next : null);
}

/**
* 创建一个新的 Pos 类
* @param obj {Object}
* @return {Pos} 坐标类
*/
static add(obj) {
    return new Pos(obj.x, obj.y);
}

/**
* 判定两个坐标是否为同一个坐标
* @param obj {Object|Pos}
* @return {Boolean} true/false
*/
same(obj) {
    return ( obj.y === this.y && obj.x === this.x );
}

待到这些完成后,我们就来实现以下道具类的基础结构。


二. 创建 Tool 类

但在我们实现一个 Tool 类的基础结构前,我们先要思考思考。

作为一个道具,它应该拥有哪些数据呢?

...

  1. 坐标:这个数据用于记录该道具位于地图的哪一位置。
  2. 大小:这个数据用于记录道具占用了地图格子的大小(1代表一整个单元格)。
  3. 半径:这个数据用于判定道具的生效范围/攻击范围。
  4. 偏移量:由于道具的大小不一定占满整个格子(或者溢出),所以我们需要这个对象来帮我们确定道具所绘制的中心点。
  5. 价格:这个数据在用户拜访该道具时,会去判定金币是否够不够,若够则扣除对应金币,不够则禁止摆放。
  6. 伤害:这个数据用于判定当道具对怪物造成伤害时,所减去怪物血量的值(怪物可以拥有抗性,以此来减免一定的伤害,但本系列不考虑这些抗性,只考虑实现最基础的功能)。
  7. 冷却时间:这个数据用于决定道具效果生效/发射攻击的时间间隔。
  8. 冷却记时:用于判定是道具是否结束冷却。
  9. 图片:用于绘制道具的图片。
  10. 首要攻击目标:判断道具优先造成伤害的目标(假设有两个怪物同处一个坐标位置,且现在道具的攻击性质为单体攻击,那么这个首要目标就会收到伤害,另一个不会,反之两者皆受伤害)
  11. 是否展示攻击范围:这个数据通常用于判定此道具是否在地图单元格里受到点击,若受到点击,则会展示其攻击范围。
  12. 角度:这个数值用于判定当前道具的枪口朝向。

至此,我们可以先来实现一下 Tool 类的基础结构

class Tool {
    constructor(pos, size, img, radius, money = 0, cd = 10, attack = 10) {
        this.pos = pos;              // 坐标
        this.size = size;           // 大小
        this.radius = radius;          // 半径

        this.offset = {
            x: (scale - size.x) / 2,   // x 偏移量
            y: (scale - size.y) / 2       // y 偏移量
        }

        this.money = money;          // 价格

        this.attack = attack;        // 伤害

        this.cd = cd;              // 冷却时间 以 1 帧为单位
        this.cdTime = 0;           // 冷却计时

        this.img = img;              // 图片
        this.first = undefined;        // 首要攻击目标
        this.showScope = false;          // 展示攻击范围

        this.angle = Math.atan2(0 - this.pos.y, 0 - this.pos.x) * (180 / Math.PI) + 90;  // 角度
    }
}

在实现了基础的 道具类结构后,我们还需要为其添加几个属性以及一个更新方法。

get type() {
    return 'tool';
}

get x() {
    return this.pos.x * scale + this.offset.x;
}

get y() {
    return this.pos.y * scale + this.offset.y;
}

/**
* 该方法后续用于获取进入了该道具攻击范围内,更新道具枪口朝向与发射子弹。
* @param state {State}
* @return void
*/
update(state) {
    // 后续会写入...
}

那么至此,我们的道具类基础结构就实现了,接下来让我们绘制一个静态的游戏面板。


三. 为 Canvas 类的 添加/修改内容(方法)。

在为 Canvas 类添加/修改内容(方法)前,我们先来看一下下面这张图里,我们需要绘制哪些东西(我用红框来区分一下)。

image.png

首先,我们先往 Canvas 类里的 更新方法添加一些内容。

update(state) {
    this.clearCanvas();
    
    // 我们把 游戏面板 数据传给 drawTextPanel 方法
    this.drawTextPanel(state.panel);
    this.drawToolBox(state.toolsBox);
    
    this.drawBtn(state);
    
    this.drawGameCells(state.map);
}

其中,drawTextPanel 方法是为绘制 游戏地图右侧 得分数据。

/**
* 绘制游戏得分数据
* @param data {Object} 道具箱对象
* @return void
*/
drawTextPanel(data) {
    let y = 30;
    
    this.siderbarX = (18 * scale + 35);
    for (let i in data) {
        this.renderFont(`${data[i].title}: ${data[i].value}`, this.siderbarX, y);
        y += 20;
    }
}

drawToolBox 方法是为绘制 游戏地图右侧 道具箱(与内部道具)的方法。

/**
* 绘制道具箱方法
* @param tools {Array} 道具箱对象
* @return void
*/
drawToolBox(tools) {
    // 绘制道具箱
    this.renderCell(this.siderbarX, 125, 90, 90, '#C8C8C8');
    
    // 绘制目前拥有的道具
    for (let i in tools) {
        this.cx.drawImage(tools[i].image, this.siderbarX + tools[i].x + tools[i].offsetX, 125 + tools[i].y, 26, scale);
    }
}

drawGameCells 方法为绘制 游戏地图单元格方法。

/**
* 绘制地图单元格方法
* @param map {Array} 地图单元格数据
* @return void
*/
drawGameCells(map) {
    for (let y in map) {
        for (let i in map[y]) {
            let color = '#fff';
            (y == 0 && i == 0 || y == map.length - 1 && i == map[y].length - 1) && (color = '#ededed');
            this.renderCell(i * scale + 20, y * scale + 20, scale, scale, '#ddd', color);
        }
    }
}

drawBtn 方法为绘制按钮方法。

/**
* 绘制按钮方法
* @param state {State} 游戏状态类
* @return void
*/
drawBtns(state) {
    this.renderCell(this.siderbarX, 230, 90, 32, '#4F4F4F', '#EFEFEF');
    this.renderFont('暂停游戏', this.siderbarX + this.padding, 250, 12);
}

至此,我们游戏面板就已经渲染完毕。

下篇,我们来实现道具的置放功能。

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