JavaScript 从零开始实现一个塔防游戏 - 01.游戏预览与准备工作

1,359 阅读4分钟

前言

几个月前我曾发表了一篇JavaScript 子弹跟踪算法的文章,其起因是因一时兴起,想实现一个 具有 “元素反应” 的塔防游戏,但要想实现一个具有 “元素反应” 的塔防游戏,那么首先我就需要先把一个最基本的塔防游戏实现出来。也因为这样,所以才有了接下来的这一系列的文章。不过话不多说,我们就先来开始着手实现吧(当然了,现在只实现最基本的,待到这个基础系列完成后,我将会以此思路实现一个具有 “元素反应” 玩法的塔防游戏)。

PS: 该游戏交互效果参考于 Github 里某个 名为 html5-tower-defense-master 的塔防游戏。

目录

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

一. 实例截图

image.png

image.png

image.png

二. 准备工作

1. HTML

<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Tower Defense</title>

    <link rel="stylesheet" href="./css/style.css">
</head>

<body>
    <div id="app">
        <div class="content" style="display: block;">
            <h2 style="margin-bottom:20px;">HTML5 塔防游戏</h2>
            <div class="footer">
                <small>HTML5 塔防游戏 © 2021 柴不是柴</small>
            </div>
            <canvas width="700" height="600"></canvas> <!-- 游戏画布 -->
        </div>
    </div>

    <script src="./js/Class/Pos.js"></script>
    <script src="./js/Class/Canvas.js"></script>
    <script src="./js/Class/State.js"></script>
    <script src="./js/Class/Tool.js"></script>
    <script src="./js/Class/Monster.js"></script>
    <script src="./js/Class/Bullet.js"></script>
    <script src="./js/Class/Event.js"></script>
    <script src="./js/app.js"></script>
    <script>
        runGame(); // 调用游戏运行程序
    </script>
</body>

</html>

2. CSS

* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

#app, #app > div {
    position: relative;
    width: 100vw;
    height: 100vh;
}

.footer {
    position: absolute;
    bottom: 0;
    height: 300px;
    left: 0;
    width: 100vw;
    background: #181818;
    color: #fff;
    padding: 20px;
}

#app .content {
    background: rgba(4, 201, 194, 0.99);
}

canvas {
    background: #fff;
}

.content {
    padding: 20px;
}

3. 图片素材

网盘:pan.baidu.com/s/1qcqVO2At…
提取码:mro1

该文件内的图片均为本人使用 AI 实现。

4. 目录结构

  • 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 (项目入口)

三. 实现辅助方法

由于本篇文章只负责做准备工作,故不论游戏逻辑。

但又考虑到后续有些方法可能需要共用/或者来启动游戏,故此我们先来实现这几个将来会用到的几个方法。

PS: 本篇文章内的所有方法均写于 app.js

1. 碰撞检测算法

考虑到目前的怪物皆为圆形怪物,攻击范围也是呈圆形的,故此我们只先需实现 圆形与圆形 之间碰撞的算法。

function collection(c1, c2) {
    return (
        Math.sqrt(Math.pow(c1.x - c2.x, 2) + Math.pow(c1.y - c2.y, 2)) < c1.r + c2.r
    );
}

2. 加载数据方法

由于我们的游戏配置数据与图片均加载于外部文件,故此我们需要实现两个加载方法。

async function getData(src) {
    return fetch(src).then(res => res.json());
}

async function loadImage(src) {
    return new Promise(resovle => {
        const image = new Image;
        image.onload = () => resovle(image);
        image.src = src;
    });
}

3. 数据克隆方法

由于之后我们可能时常需要用到某个数据,并且在不能污染它的情况下,我们可以先实现一个深度克隆数据的方法。

function deepClone(arr) {
    return JSON.parse(JSON.stringify(arr));
}

4. 游戏启动方法

既然是一个游戏,那么它必然少不了运行方法。

但在游戏运行起来前,我们需要加载一下道具图片与地图数据。

function runGame() {
    // 这里我们使用 Promise.all 来同步加载多个文件。
    Promise.all([
        loadImage('./images/tool.svg'),
        getData('./js/Data/Config.json')
    ]).then(([tool, config_data]) => {
        let canvas = new Canvas(), // 这里我们创建一个画布类
            state = new State(config_data, 'playing', tool); // 这里我们创建一个 游戏状态类

        /*
        这里我们调用一个每帧都刷新的方法,并且传回一个回调函数,
        其目的就是当游戏状态变为 结束时,我们终止游戏的运行
        */
        return requestAnimationFrameFunc(() => {
            // 如果游戏状态为正在运行时
            if (state.status === 'playing') {
                state.update(); // 此为游戏状态更新方法
                canvas.update(state); // 此为画布渲染方法
            // 如果游戏状态为结束时
            } else if (state.status === 'over') {
                console.log('Game Over'); // 控制台输出 Game Over
                return true; // 返回 true 终止游戏循环
            }
            
            // 如果游戏状态为暂停时,即既不是运行与结束两种状态时,则什么都不执行
        });
    });
}

function requestAnimationFrameFunc(callback) {
    function frame() {
        if (callback()) return; // 当回调函数返回结果为 true 时, 游戏终止循环
        requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);
}

那么至此,我们的准备工作基本都完成了。

下篇,我们就来实现游戏画布类与游戏状态类的基础结构。

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