塔防游戏中的敌人如何沿路径前进 (JavaScript 实现)

如果开发一个塔防游戏,很自然的会遇上这么两个名字很像的问题:

  • Path-finding: 如果知道起点和终点,如何在其间找到一条路径
  • Path-following: 已知从起点到终点的路径,物体如何才能沿着它行进

本文将要讨论的是第二个问题 path following,给定一条路径,看物体如何沿着它从起点运行至终点。为了方便描述,接下来的内容中,用单词 Boid 来表示行进的物体或塔防中的敌人。

接下来会用一种简单的方法来解决这一问题,最终完成的代码库可见 GitHub: boid-path-following,repo 的多个分支对应了文中的不同步骤。

准备工作

先来看看如何标识出画面中的位置,首先画面被一系列的横纵线分成了许多网格,对于地图范围内的一个点,它会有自己的像素坐标 (x, y),同时它所处的格子也有自己的坐标 (col, row) 或 (xIndex, yIndex),表示所处的列和行。

为了区分,下文中提到像素坐标即为用像素表示的坐标,网格坐标表示点在网格中的列和行。

在这种表示方法下,还需要一个工具函数 index2Px(col, row),用于计算格子中心的像素坐标。

接下来给出路径的坐标,路径是如下的一个二维数组:

    const path = [[0, 1], [COLS - 4, 1], [COLS - 4, 4], [6, 4], [6, 7], /* 部分省略 */]

每一项都是路径上一个点的网格坐标,将这些点用直线连接起来后就得到了 boid 行进的路径。我们的目标就是要让 boid 能够从路径第一个坐标移动至最后一个坐标。

Boid 如何沿直线前进

先考虑最简单的问题,如何让 Boid 沿着一条直线行进。

物体的移动需要位置和速度,为了表示其像素坐标,boid 需要 x, y 属性;其速度需要 speed 属性,同时还需要一个 angle,以便计算出速度在两个方向上的分量 vx, vy

动画效果的实现需要用 requestAnimationFrame 函数,每一秒为60帧,每一帧中都会执行一次循环,在其中改变位置:

下一时刻的位置 = 当前时刻的位置 + 速度
    // 示意代码
    // Boid 类的 step() 方法
    step() {
        const speed = this.speed;
        const angle = Math.PI / 2;

        this.vx = Math.cos(angle) * speed;
        this.vy = Math.sin(angle) * speed;

        // 如果 vx, vy 不变化,则会沿一条直线前进
        this.x += this.vx;
        this.y += this.vy; 
    }

在每一个循环中,boid 的位置都会发生变化,在新的位置上将其画出即可看到 boid 沿直线运动的效果。

这一部分可在示例代码库的 demo01/go-straight 分支上查看:

git checkout demo01/go-straight
npm run demo01

现在 boid 已经动起来了,但是却没法停止,这就是我们接下来需要考虑的问题。

如何让 boid 在目标点处停止

要让 boid 能够知道自己到达了目标点,则在每一次循环过程中,需要计算出此刻离目标点的距离分量 dxdy,据此算出距离 dist,将其与速度 speed 进行比较。如果 dist > speed,说明物体离目标点还挺远,继续将速度加到位置上即可。反之则表明物体将要到达终点,此时若直接加上速度,boid 可能会越过目标点,因此需要一点不同的处理。

    // 示意代码
    step() {
        if (reachDest) {
            // 已到达终点,可根据实际需要进行操作            
        }

        const speed = this.speed;

        // 与目标点的距离
        this.dx = target.x - this.x;
        this.dy = target.y - this.y;
        this.dist = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
        this.angle = Math.atan2(this.dy, this.dx);

        // 速度分量
        this.vx = Math.cos(this.angle) * speed;
        this.vy = Math.sin(this.angle) * speed;

        if (this.dist > speed) {
            this.x += this.vx;
            this.y += this.vy; 
        } else {
            // 当前时刻的位置加上速度后超过了当前目标点
            // 物体下一时刻将处于当前目标点的位置
            this.x = target.x;
            this.y = target.y;
            this.reachDest = true;
        }
    }

这一部分可在示例代码库的 demo01/stop 分支上查看:

git checkout demo01/stop
npm run demo01

此时,到达了终点的 boid 被清除而不再显示。

如何让 boid 能够转向

前面叙述中为了简化,路径中只有起点和终点,所以 boid 没有机会转向,那当路径变复杂了之后,boid 该如何运动?

前面已经提到过,path 是一个记录了路径网格坐标的数组,boid 会从中取一个坐标作为自己的当前目标点,然后一直向前行进,到达了这个目标点之后,它会从 path 数组中取出下一个坐标,继续移动至该位置。循环以上过程,直到 boid 到达 path 中的最后一个坐标。

上面的代码中,我们的目标点 target 固定为 path 的最后一个坐标,而现在每一次转向时 target 都会变化,所以加入这样的两个变量:

  • waypoint 表示当前目标点的索引
  • angleFlag 记录是否需要转向。

// Boid 的 step() 中的部分示意代码

/* 每次转向后目标点需要重新计算 */
const waypoint = path[this.waypoint];   // 当前目标点的网格坐标
const target = index2Px(...waypoint);   // 当前目标点的像素坐标

// ...

// 判断是否需要转向,如果需要转向,则重新计算角度
if (this.angleFlag) {
    this.angle = Math.atan2(this.dy, this.dx);
    this.angleFlag = 0;
}

// 每次到达一个目标点之后,都要检查是否为终点
if (this.waypoint + 1 >= path.length) {
    // 到达终点
    this.reachDest = true;
} else {
    this.waypoint++;
    this.angleFlag = 1;
}

这一部分可在示例代码库的 demo01/steering 分支上查看:

git checkout demo01/steering
npm run demo01

结果可见下图:

到此为止,这种 boid 沿路径行进的方法已经讲解完毕了。建议读者查看一下 repo 中的代码,自己修改部分代码,比如更改路径,看结果会有何不同。

其它的方法

这一种方法中的确实现了沿路径移动的效果,但是有点儿单调,boid 只能在路径的中轴线上移动,而且它们之间也没有交互的效果。The Nature of Code 这本书的第六章 Autonomous Agents 中介绍了另一种稍微复杂的方法来实现 path following。

我之前参考他人的代码实现了这种方法的一个演示版本,其代码在此处

(也许之后会补一篇博客来介绍 The Nature of Code 中的实现,但谁知道会不会写呢🤔)

结语

最后,我最近在写的这个塔防游戏中就使用了本文介绍的 path following 方法。虽然游戏还没完成,但点进去看看再给个 star 又不费电😀。


本文地址:塔防游戏中的敌人如何沿路径前进 (JavaScript 实现)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容