手把手教你写抖音爆火小游戏之激战突围(五):反派驾到 —— 敌人的AI与生成

阅读前请先下载项目源码,边读边看源码以加深理解和实操,
源码地址已放于文章末尾!

各位游戏世界的缔造者们,欢迎回来!在前四章里,我们从零开始,不仅搭建了游戏世界,创造了听从指挥的英雄,还为他们配备了足以毁天灭地的火炮。现在,万事俱备,只欠……敌人!

在这里插入图片描述

一个好的对手,才能成就一个伟大的英雄。今天,我们就来深入探讨游戏中这些“可爱又迷人”的反派角色——敌人们,是如何被创造出来,并拥有“智能”的。

第一步:敌人的“出生点” —— 道路尽头的召唤

敌人不是凭空出现的。在我们的游戏里,敌人的生成是由 PathBasic 类触发的,这个类我们后面讲关卡生成时还会细聊,现在你只需要知道,它代表了我们拼接起来的一小段路。当玩家的队伍走到某一段路的尽头时,PathBasic 就会触发一个事件,通知 RoleLayer:“嘿,大佬,该在这儿刷一波怪了!”

assets/Game/Script/Custom/road/PathBasic.ts 核心代码:

update() {
    // ...
    // 判断玩家是否到达了路的尽头
    if (dist <= 0.1) { 
        if (!this.isCreateEnemy) {
            this.isCreateEnemy = true;
            // 重点:发送“创建敌人”事件,把预设的敌人数据enemyId传过去
            EventManager.emit(EventTypes.CurLevelEvents.createEnemys, this.enemyId);
        }
    }
    // ...
}

RoleLayer 作为角色总管,当然时刻在监听这个事件。一旦听到召唤,它就会开始执行创建敌人的逻辑。

第二步:高效的“造人工厂” —— delayCreateEnemy

如果一次性把几十个敌人全部创建出来,就算用了对象池,也可能会因为瞬间计算量过大导致游戏卡顿一下。所以,项目作者用了一个非常聪明的方法:分帧创建

assets/Game/Script/Custom/RoleLayer.ts - createEnemysdelayCreateEnemy:

// 监听到 createEnemys 事件后,调用此方法
protected createEnemys(d) {
    // d 是从 PathBasic 传过来的敌人配置数据
    // ...
    // 把需要创建的敌人信息,先存到一个待处理列表 _delayCreateEnemy里
    this._delayCreateEnemy.push({
        id: id,         // 敌人类型
        path: path,     // 它应该走哪条路
        count: count,   // 创建几个
        // ... 其他参数
    });
}

// 在 RoleLayer 的 update 方法里,每帧都会调用
protected delayCreateEnemy() {
    if (this._delayCreateEnemy.length > 0) {
        // 每次只处理待办列表里的第一个任务
        let e = this._delayCreateEnemy[0];

        // --- 核心逻辑:一帧只创建一个敌人 ---
        if (e.curCount < e.count) {
            this.createEnemy(e.id, e.path, ...); // 调用真正的创建方法
            e.curCount++;
        } else {
            // 这个批次的敌人创建完了,就从待办列表里移除
            this._delayCreateEnemy.shift();
        }
    }
}

代码解读

  • 缓冲队列createEnemys 方法并不直接创建敌人,而是把“创建任务”放进一个叫 _delayCreateEnemy 的数组里,像一个“待办清单”。
  • 逐帧执行:在 update 里调用的 delayCreateEnemy,每一帧只从“待办清单”里拿出一个任务,并且只创建一个敌人实例,然后就收工等待下一帧。
  • 效果:这样就把一个可能导致卡顿的“瞬时大任务”,平摊到了几十甚至上百帧里去完成,玩家几乎毫无察觉,游戏体验如丝般顺滑。这个技巧在优化游戏性能时非常实用!

第三步:敌人的“灵魂”与“大脑”—— Enemy.ts 与状态机

敌人的创建过程和玩家类似,也是从对象池里拿出 Node,然后 new Enemy() 注入灵魂。但敌人的“灵魂”显然比玩家要复杂,因为它需要一套自己的行为逻辑,也就是我们常说的 AI (人工智能)

在这个游戏里,敌人的AI是基于一个经典的**有限状态机(Finite State Machine, FSM)**来实现的。

我画了一张图来展示敌人的几种核心状态和切换关系:

创建后
游戏开始
玩家进入警戒范围
到达关卡终点
玩家离开警戒范围
被击杀
被击杀
回收进对象池
Idle
沿预设路径移动
追击玩家
进入终点区域
死亡状态

这套状态机赋予了敌人基本的“智能”:

  1. Move (移动状态):敌人被创建后,会沿着一条预设的、独一无二的路径前进。这条路径是在创建时由 RoleLayer 动态计算生成的,保证了敌人不会傻乎乎地走直线。
  2. Follow (追击状态):在 Move 状态下,敌人会不断检测玩家是否进入了它的“警戒范围”。一旦发现玩家,它就会立刻放弃原来的路径,切换到 Follow 状态,开始朝玩家的位置冲过去。如果玩家又跑远了,它可能会切换回 Move 状态,继续走原来的路。
  3. EnterPass (终点状态):如果敌人足够幸运,没有被玩家干掉,并且走到了关卡路径的尽头,它就会进入这个状态,并对我们的基地造成伤害。
  4. Death (死亡状态):当敌人的血量 hp 降到0时,它就会进入死亡状态,播放死亡动画和特效,然后光荣地“退休”,回到对象池里。

assets/Game/Script/Custom/Enemy.ts - byHit 方法:

/** 被击中 */
byHit(atk: number) {
    if (this._curState == EnemyStateType.Death) return; // 已经死了,就别鞭尸了

    this.hp -= atk; // 扣血

    // 播放被击中特效和音效
    this.playHitEffect(); 
    AudioSystem.playEffect(AudioEnum.hit);
    
    // 触发一个“伤害数字”UI的显示事件
    EventManager.emit(EventTypes.UIEvents.showDamage, this.node.worldPosition, atk);

    if (this.hp <= 0) {
        // 血量小于0,切换到死亡状态
        this.changeState(EnemyStateType.Death);
    }
}

代码解读
byHit 方法是敌人与我们的子弹交互的唯一接口。当子弹碰到敌人,就会调用这个方法。它清晰地展示了敌人受击后的反应:扣血 -> 播放声光特效 -> 显示伤害数字 -> 判断是否死亡

第四步:难度升级的秘密 —— Enemy.initProp

为了让游戏更有挑战性,敌人肯定不能一成不变。随着玩家通过的关卡越来越多,敌人的血量和攻击力也应该动态提升。这个秘密就藏在 Enemy.initProp 方法里。

assets/Game/Script/Custom/Enemy.ts - initProp 方法:

initProp() {
    // 从存档系统获取当前是第几关
    let lv = StorageSystem.getData().levelAssets.curLv;
    
    // 核心公式:根据关卡等级,计算出一个增长率
    let rate = (lv * GlobalConfig.EnemyCfg.lvupStep + 1);

    // 从全局配置里读取该类型敌人的基础属性
    const cfg = GlobalConfig.Enemy[this.enemyType];
    
    // 最终属性 = 基础属性 * 增长率
    this.hp = cfg.hp * rate;
    this.atk = cfg.atk * rate;
    this.curScale = cfg.scale; // 体型大小
}

代码解读
这是一个简单但非常有效的数值动态生成系统。

  • GlobalConfig.EnemyCfg.lvupStep 是一个“难度系数”,比如设置为 0.1,意味着每过一关,敌人的属性就增长10%。
  • 每次敌人从对象池里被“激活”时,都会调用 initProp 方法,根据当前的关卡数,重新计算自己的战斗力。
  • 这样一来,我们只需要设计好敌人的基础数值和难度系数,游戏就可以自动地、源源不断地生成越来越强的敌人,让玩家始终保持挑战的乐趣。

今日总结

今天我们成功地召唤出了游戏中的反派大军!我们不仅学习了如何高效地、不卡顿地动态生成大量敌人,还深入剖E析了赋予它们“智能”的核心技术——有限状态机。

我们掌握了:

  1. 分帧创建:一个避免游戏卡顿的实用性能优化技巧。
  2. 有限状态机 (FSM):如何用它来设计和管理一个角色的复杂行为逻辑 (AI)。
  3. 数据驱动:如何通过外部配置和关卡进度,动态地调整游戏难度。

现在,战场上已经“敌我分明”,英雄们有了需要消灭的目标,敌人们也有了冲锋的方向。一场真正的大战,即将拉开帷幕!

但是,你有没有想过,我们现在看到的这条让主角和敌人奔跑的道路,以及两旁的建筑,究竟是怎么来的?难道是美术同学一段一段手动拼接到场景里的吗?当然不是!

下一期,我们将揭开这个游戏世界构建的终极秘密——程序化生成。我们将学习如何仅凭一份数据文件,用代码动态地、随机地创造出无穷无尽的游戏关卡!

准备好扮演“创世神”了吗?敬请期待!不见不散!

游戏源码下载地址:
https://wwuj.lanzoul.com/i4N1n33e9dib
验证码hack

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

THMAIL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值