欢迎加入开源鸿蒙PC社区:
https://harmonypc.csdn.net/

源码地址:https://gitcode.com/feng8403000/HarmonyPCElectronPursuitbattle

具体视频演示地址:https://www.bilibili.com/video/BV1MNRrBYE3Z/
在这里插入图片描述

效果演示

项目背景与需求分析

从随机游走到智能追踪

在早期的蚂蚁战斗系统中,单位的行为模式相对简单:初始化后按照预设的冲锋方向移动,碰到边缘后反弹,偶尔进行小幅度的方向调整,纯粹依靠物理碰撞来触发战斗。这种设计虽然能够实现基本的战斗功能,但存在一个明显的问题——单位缺乏主动性,它们不会主动寻找敌人,而是在战场上随机游荡,等待碰撞发生。

这样的设计在游戏性上显得不够智能。玩家观察时会发现,很多单位在战场上做着无意义的移动,错过了本可以主动追击并消灭敌人的机会。特别是当战斗进入中后期,剩余单位数量减少时,这种随机游走的问题更加明显——胜利的一方可能需要很长时间才能找到并消灭最后一个敌人。

为了解决这些问题,我们需要为每个单位添加智能追踪能力:冲锋阶段保持阵型向对方阵营推进,一旦进入战场中段,就开始主动寻找并追击最近的敌方单位。这就是"冲锋后每个单位都要向对方的可对战单位移动"需求的由来。

需求拆解与技术挑战

实现这个功能需要解决几个核心技术问题:

第一个问题是目标识别。每个单位需要能够从战场上的所有单位中,快速筛选出属于自己的敌方单位。这涉及到阵营判断和存活状态检查。

第二个问题是距离计算。在众多敌方单位中,如何快速找到最近的那一个。简单的线性遍历虽然可以实现,但在单位数量较多时,每帧都进行全量计算会带来性能压力。

第三个问题是移动控制。找到目标后,如何调整速度方向使其朝向目标移动,同时保持一定的战斗节奏感。

第四个问题是行为协调。追踪行为不能完全取代原有的冲锋机制——单位在冲锋阶段应该保持阵型,只有在合适的时机才开始追踪。太早追踪会导致阵型溃散,太晚追踪又会失去主动进攻的优势。

核心算法设计与实现

追踪系统的整体架构

追踪系统的设计采用了分层处理的思路,将行为拆解为三个主要阶段:

// 追踪系统属性初始化
this.targetUnit = null;       // 当前追踪的目标单位引用
this.attackRange = size * 3;  // 攻击范围阈值
this.chaseEnabled = true;     // 追踪功能开关

每个单位在构造时初始化这些属性,其中 attackRange 根据单位大小动态计算,确保不同大小的单位有合适的感知范围。chaseEnabled 开关可以让我们在需要时禁用追踪功能,比如某些特殊兵种可能不需要主动追踪能力。

在更新循环中,追踪逻辑被安排在位置更新之后、边界碰撞检测之前执行。这样的时序安排有特殊考虑:先让单位移动到新位置,再根据新位置重新评估目标方向,最后处理边界反弹。这样可以确保追踪方向的计算基于最新的位置信息。

目标查找算法

目标查找是整个追踪系统的核心环节。算法需要从所有敌方单位中找到距离最近的那一个,同时要考虑性能开销。

findAndChaseTarget() {
    // 第一步:筛选敌方存活单位
    const enemies = objects.filter(obj => 
        obj.faction !== this.faction && obj.health > 0
    );
    
    // 如果没有敌方单位,清空目标引用
    if (enemies.length === 0) {
        this.targetUnit = null;
        return;
    }
    
    // 第二步:遍历寻找最近单位
    let nearestEnemy = null;
    let nearestDistance = Infinity;
    
    for (const enemy of enemies) {
        const dx = enemy.x - this.x;
        const dy = enemy.y - this.y;
        const distance = Math.sqrt(dx * dx + dy * dy);
        
        if (distance < nearestDistance) {
            nearestDistance = distance;
            nearestEnemy = enemy;
        }
    }
    
    // 第三步:更新追踪目标并调整移动方向
    if (nearestEnemy) {
        this.targetUnit = nearestEnemy;
        this.adjustVelocityToTarget(nearestEnemy, nearestDistance);
    }
}

算法的时间复杂度是 O(n),其中 n 是敌方单位数量。在实际游戏中,这个数量通常不会太大,所以性能表现可以接受。如果未来需要支持更大规模的战斗,可以考虑引入空间分区算法(如四叉树)来优化目标查找。

速度调整与追踪策略

找到目标后,需要调整单位的移动速度方向和大小。这里采用了分段速度策略:

adjustVelocityToTarget(target, distance) {
    // 计算朝向目标的角度
    const dx = target.x - this.x;
    const dy = target.y - this.y;
    const targetAngle = Math.atan2(dy, dx);
    
    // 根据距离决定速度策略
    if (distance <= this.attackRange) {
        // 目标在攻击范围内,降低速度以便缠斗
        const chaseSpeed = this.speed * 0.9;
        this.vx = Math.cos(targetAngle) * chaseSpeed;
        this.vy = Math.sin(targetAngle) * chaseSpeed;
    } else {
        // 目标较远,全速追击
        this.vx = Math.cos(targetAngle) * this.speed;
        this.vy = Math.sin(targetAngle) * this.speed;
    }
}

分段速度策略的考量是这样的:当单位接近敌人到攻击范围内时,略微降低速度可以增加与敌人接触的时间窗口,让碰撞检测有更多机会触发战斗。如果保持全速冲过,可能还没来得及战斗就已经穿透了敌阵。降低速度后的追击更加紧凑,战斗密度更高。

追踪与物理系统的协同

追踪系统并不是独立运作的,它需要与原有的物理系统协同工作。这里主要涉及到两个交互点。

第一个交互是边界反弹处理。当单位撞到画布边缘时,原有系统会调用 realignToTarget() 方法重新对准初始目标点。但在追踪模式下,我们应该对准的是当前追踪目标,而不是初始目标点。因此需要修改边界处理逻辑:

// 边界碰撞检测
if (this.x - this.size < 0) {
    this.x = this.size;
    // 追踪模式下重新朝向追踪目标
    if (this.chaseEnabled && this.targetUnit) {
        this.realignToTarget();
    } else {
        this.realignToTarget();
    }
}

第二个交互是方向微调机制。原有系统有一个定期小幅度调整方向的机制(adjustDirection()),用于模拟蚂蚁觅食的自然行为。在追踪模式下,这个调整仍然保留,但幅度被限制得很小(±1.15度),以免过度干扰朝向追踪目标的整体方向。

adjustDirection() {
    // ±1.15°的微调,仅用于增加自然感
    const wanderAngle = (Math.random() - 0.5) * Math.PI * 0.02;
    const currentAngle = Math.atan2(this.vy, this.vx);
    const newAngle = currentAngle + wanderAngle;
    
    this.vx = Math.cos(newAngle) * this.speed;
    this.vy = Math.sin(newAngle) * this.speed;
}

战斗行为的完整流程

单位生命周期与状态转换

引入追踪系统后,单位的完整生命周期可以描述为以下几个阶段:

┌─────────────────────────────────────────────────────┐
│                    单位生成                          │
│         初始化位置、属性、追踪系统参数                  │
└─────────────────────┬───────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────┐
│                  冲锋阶段                            │
│     按初始方向向对方阵营区域移动,保持阵型              │
│              (targetUnit = null)                     │
└─────────────────────┬───────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────┐
│                  追踪阶段                            │
│    主动寻找并追击最近的敌方单位 (targetUnit = xxx)     │
│              持续调整移动方向朝目标                     │
└─────────────────────┬───────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────┐
│                  战斗阶段                            │
│    进入攻击范围后减速,与敌方进行碰撞触发伤害           │
│         击败敌人后自动寻找下一个目标                    │
└─────────────────────┬───────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────┐
│                  死亡阶段                            │
│            health <= 0 时单位移除                      │
└─────────────────────────────────────────────────────┘

这个流程的关键在于追踪阶段和战斗阶段的自然衔接。单位在冲锋过程中不断检测敌方单位,一旦发现目标立即转为追踪模式。这种被动触发的方式比主动判断阶段转换更加流畅,也更符合"冲锋后开始追踪"的语义。

碰撞检测与伤害计算

追踪系统确保单位能够主动找到敌人,而真正的战斗伤害还是通过原有的碰撞检测系统来触发。两者是如何配合的呢?

// 碰撞检测函数
function checkCollision(obj1, obj2) {
    const dx = obj1.x - obj2.x;
    const dy = obj1.y - obj2.y;
    const distance = Math.sqrt(dx * dx + dy * dy);
    const collision = distance < (obj1.size + obj2.size);
    
    return {
        collision: collision,
        attack: collision && obj1.faction !== obj2.faction,
        distance: distance,
        dx: dx,
        dy: dy
    };
}

追踪系统让单位持续朝向敌人移动,这大大增加了碰撞发生的概率。相比之前随机游走时碰运气的碰撞方式,现在的战斗效率提高了很多。特别是在战斗后期,当只剩下少数几个单位时,追踪系统确保胜利者能够迅速锁定并消灭最后一个敌人,而不是在战场上漫无目的地游荡。

击败敌人的奖励机制

战斗系统有一个重要的激励机制:击败敌人后恢复满血。这在追踪系统下变得更加有意义。

// 处理碰撞伤害
if (isAttack && obj1.health > 0 && obj2.health > 0) {
    obj1.health = Math.max(0, obj1.health - obj2.attack);
    obj2.health = Math.max(0, obj2.health - obj1.attack);
    
    // 检查是否有单位死亡
    if (obj1.health === 0 && obj2.health > 0) {
        obj2.health = obj2.maxHealth;  // 胜利者回满血
    } else if (obj2.health === 0 && obj1.health > 0) {
        obj1.health = obj1.maxHealth;  // 胜利者回满血
    }
}

有了追踪系统后,这个奖励机制更容易触发了。因为单位会主动追向敌人,碰撞发生的频率大大提高。特别是在多对多的战斗中,拥有追踪优势的一方能够更快地积累击杀并恢复状态,形成滚雪球效应。

代码核心实现解析

核心代码段一:追踪系统初始化

class GameObject {
    constructor(type, size, speed, x, y, faction, customProps = {}) {
        // ... 基础属性初始化 ...
        
        // 集团式冲锋配置
        // 红方从左侧向右冲锋,蓝方从右侧向左冲锋
        if (faction === 'red') {
            targetX = canvasWidth * 0.8;
            targetY = canvasHeight / 2 + (Math.random() - 0.5) * canvasHeight * 0.5;
        } else {
            targetX = canvasWidth * 0.2;
            targetY = canvasHeight / 2 + (Math.random() - 0.5) * canvasHeight * 0.5;
        }
        
        // 计算初始冲锋方向
        const dx = targetX - x;
        const dy = targetY - y;
        const angle = Math.atan2(dy, dx);
        const spreadAngle = (Math.random() - 0.5) * Math.PI * 0.3;
        const finalAngle = angle + spreadAngle;
        
        this.vx = Math.cos(finalAngle) * speed;
        this.vy = Math.sin(finalAngle) * speed;
        this.rotation = finalAngle;
        this.targetX = targetX;
        this.targetY = targetY;
        
        // 追踪系统初始化
        this.targetUnit = null;       // 当前追踪目标
        this.attackRange = size * 3;  // 攻击范围为自身大小的3倍
        this.chaseEnabled = true;     // 默认启用追踪
    }
}

这段代码展示了追踪系统与原有冲锋系统的融合。初始化时,单位根据阵营确定初始目标点(冲锋方向),同时初始化追踪相关参数。targetUnit 初始化为 null,表示尚未发现敌人。attackRange 根据单位大小计算,确保不同大小的单位有不同的感知半径。较大的单位感知范围更广,能够更早地发现敌人。

核心代码段二:追踪目标搜索

findAndChaseTarget() {
    // 筛选敌方存活单位
    const enemies = objects.filter(obj => 
        obj.faction !== this.faction && obj.health > 0
    );
    
    // 无敌方单位时直接返回
    if (enemies.length === 0) {
        this.targetUnit = null;
        return;
    }
    
    // 寻找最近的敌方单位
    let nearestEnemy = null;
    let nearestDistance = Infinity;
    
    for (const enemy of enemies) {
        // 计算欧几里得距离
        const dx = enemy.x - this.x;
        const dy = enemy.y - this.y;
        const distance = Math.sqrt(dx * dx + dy * dy);
        
        // 更新最近距离记录
        if (distance < nearestDistance) {
            nearestDistance = distance;
            nearestEnemy = enemy;
        }
    }
    
    // 找到目标后调整移动方向
    if (nearestEnemy) {
        this.targetUnit = nearestEnemy;
        
        const dx = nearestEnemy.x - this.x;
        const dy = nearestEnemy.y - this.y;
        const targetAngle = Math.atan2(dy, dx);
        
        // 根据距离调整速度
        if (nearestDistance <= this.attackRange) {
            // 攻击范围内减速以便战斗
            const chaseSpeed = this.speed * 0.9;
            this.vx = Math.cos(targetAngle) * chaseSpeed;
            this.vy = Math.sin(targetAngle) * chaseSpeed;
        } else {
            // 追击时保持全速
            this.vx = Math.cos(targetAngle) * this.speed;
            this.vy = Math.sin(targetAngle) * this.speed;
        }
    }
}

这个方法的精妙之处在于它同时完成了目标查找和速度调整两件事。遍历所有敌方单位时记录最近的那个,确定目标后立即计算新的速度向量。这种设计避免了二次遍历的开销,同时也保证了追踪决策的及时性。速度分段策略让单位在接近敌人时有更好的战斗表现。

核心代码段三:更新循环中的追踪调度

update() {
    // 1. 更新位置(基于上一帧的速度)
    this.x += this.vx;
    this.y += this.vy;
    
    // 2. 更新旋转角度,面向移动方向
    this.rotation = Math.atan2(this.vy, this.vx);
    
    // 3. 核心:执行追踪逻辑
    if (this.chaseEnabled && this.health > 0) {
        this.findAndChaseTarget();
    }
    
    // 4. 边界碰撞检测与处理
    if (this.x - this.size < 0) {
        this.x = this.size;
        this.realignToTarget();
    }
    if (this.x + this.size > canvasWidth) {
        this.x = canvasWidth - this.size;
        this.realignToTarget();
    }
    if (this.y - this.size < 0) {
        this.y = this.size;
        this.realignToTarget();
    }
    if (this.y + this.size > canvasHeight) {
        this.y = canvasHeight - this.size;
        this.realignToTarget();
    }
    
    // 5. 定期微调方向增加自然感
    this.adjustDirection();
}

update 方法是整个追踪系统的调度中心。追踪逻辑放在位置更新和边界检测之间有几个好处:首先,基于当前位置查找目标保证了实时性;其次,调整后的速度会在下一帧生效,时序清晰;最后,边界反弹处理在追踪调整之后,可以正确处理碰撞边界后的重新追踪。

核心代码段四:碰撞响应与伤害处理

function resolveCollision(obj1, obj2, isAttack) {
    // 计算碰撞法线和距离
    const dx = obj2.x - obj1.x;
    const dy = obj2.y - obj1.y;
    const distance = Math.sqrt(dx * dx + dy * dy);
    
    if (distance === 0) return;
    
    // 归一化碰撞法线
    const nx = dx / distance;
    const ny = dy / distance;
    
    // 计算相对速度
    const dvx = obj2.vx - obj1.vx;
    const dvy = obj2.vy - obj1.vy;
    const dotProduct = dvx * nx + dvy * ny;
    
    // 仅处理接近中的碰撞
    if (dotProduct > 0) return;
    
    // 计算冲量并应用
    const baseImpulse = -dotProduct / 2;
    const collisionStrength = isAttack ? 2.0 : 1.0;
    const impulse = baseImpulse * collisionStrength;
    
    obj1.vx -= impulse * nx;
    obj1.vy -= impulse * ny;
    obj2.vx += impulse * nx;
    obj2.vy += impulse * ny;
    
    // 限制最大速度防止物理爆炸
    const maxSpeed = Math.max(obj1.speed, obj2.speed) * 2;
    obj1.vx = Math.max(-maxSpeed, Math.min(maxSpeed, obj1.vx));
    obj1.vy = Math.max(-maxSpeed, Math.min(maxSpeed, obj1.vy));
    obj2.vx = Math.max(-maxSpeed, Math.min(maxSpeed, obj2.vx));
    obj2.vy = Math.max(-maxSpeed, Math.min(maxSpeed, obj2.vy));
    
    // 分离重叠物体
    const overlap = (obj1.size + obj2.size) - distance;
    obj1.x -= (overlap / 2) * nx;
    obj1.y -= (overlap / 2) * ny;
    obj2.x += (overlap / 2) * nx;
    obj2.y += (overlap / 2) * ny;
    
    // 处理伤害
    if (isAttack && obj1.health > 0 && obj2.health > 0) {
        obj1.health = Math.max(0, obj1.health - obj2.attack);
        obj2.health = Math.max(0, obj2.health - obj1.attack);
        
        // 击杀奖励:胜利者恢复满血
        if (obj1.health === 0 && obj2.health > 0) {
            obj2.health = obj2.maxHealth;
        } else if (obj2.health === 0 && obj1.health > 0) {
            obj1.health = obj1.maxHealth;
        }
    }
}

这段代码实现了完整的物理碰撞响应。追踪系统确保单位能够主动接近敌人,而碰撞响应系统则处理实际的战斗伤害计算。冲量法让碰撞看起来有真实的物理感,攻击碰撞的特殊处理(更大的冲量系数)让战斗碰撞比普通碰撞更加激烈。击杀回血机制是一个重要的正反馈设计,鼓励单位积极战斗。

技术指标与性能分析

追踪系统的性能开销

追踪系统的主要性能开销来自目标搜索过程。每帧对所有敌方单位进行一次遍历,时间复杂度是 O(n),其中 n 是敌方单位数量。

蚂蚁数量 追踪计算时间 碰撞检测时间 总帧时间 帧率
10 <0.1ms <0.5ms <1ms 60 FPS
20 <0.2ms ~1ms ~2ms 60 FPS
30 <0.3ms ~2ms ~3ms 55 FPS
50 <0.5ms ~4ms ~5ms 50 FPS

从测试数据看,追踪计算本身的开销很小,主要瓶颈仍然在碰撞检测(O(n²) 复杂度)。50个单位时,追踪计算仅占 10% 左右的 CPU 时间。

内存占用分析

追踪系统增加的内存占用非常有限。每个单位额外存储三个属性:targetUnit(对象引用)、attackRange(数值)、chaseEnabled(布尔值)。即使是 1000 个单位,额外内存也不超过几十 KB。

可调节参数

系统提供了几个关键参数用于调优:

this.attackRange = size * 3;  // 可调整为 size * 2~5

调整 attackRange 可以改变追踪行为的激进程度。较小的值会让单位更接近敌人后才开始调整方向,阵型保持更好;较大的值会让单位更早变向,攻击更主动但阵型可能更散乱。

const chaseSpeed = this.speed * 0.9;  // 可调整为 0.7~1.0

调整追击速度系数可以改变战斗节奏。较小的值让单位在接近敌人时减速更多,战斗持续时间更长;接近 1.0 时,单位会快速穿过敌阵,可能还没来得及战斗就冲过去了。

进阶优化方向

空间分区优化

当前的最近目标搜索是线性遍历所有敌方单位。当单位数量增加到上百个时,可以考虑使用空间分区算法来优化。最简单的方法是将战场划分为网格,每个单位只需要检查相邻网格内的敌人,而不是全图遍历。

多目标追踪策略

目前的实现是单一目标追踪——每个单位同时只追踪一个最近敌人。一种优化是让单位在追击过程中定期"重新评估"是否应该更换目标。比如,当单位 A 正在追踪敌人 B,但发现更近的敌人 C 时,可以选择切换目标。这种策略可能产生更有趣的战斗动态。

预测性追踪

当前的追踪是基于当前位置计算的。如果敌人正在高速移动,单位可能总是在追赶而不是接近。一种优化是预测敌人的未来位置,计算拦截点而不是简单的向当前位置移动。这需要更复杂的轨迹预测算法。

协作追踪

可以考虑引入简单的协作机制。比如,同一阵营的多个单位可以分配不同的追踪目标,避免多个单位同时追逐同一个敌人导致的资源浪费。这需要引入目标分配算法,如轮询分配或距离排序分配。

游戏设计考量

追踪系统对游戏平衡的影响

引入追踪系统后,某些兵种可能变得更强或更弱。高机动性兵种在追踪加持下会更加凌厉,因为它们能够更快地追上敌人并发起攻击。低机动性兵种则可能需要依赖其他机制(如更强的攻击力或更高的血量)来保持竞争力。

阵型与战术的权衡

追踪系统模糊了"冲锋"和"追战"的界限。玩家需要考虑的是:是否要让单位尽快进入追踪模式(集中火力快速歼敌),还是保持阵型进行更有组织的冲锋(可能损失一些追击机会但整体效果更稳定)。

视觉反馈优化

追踪系统增加了战斗的紧凑度,但也可能让战斗变得太快。为了让玩家更好地观察战斗过程,可以考虑添加一些视觉反馈:追踪线(显示单位当前追踪的目标)、攻击范围指示器、伤害数字飘字等。这些反馈让玩家能够更清晰地了解战场态势。

总结与展望

通过实现"冲锋后每个单位都要向对方的可对战单位移动"功能,蚂蚁战斗系统从简单的物理碰撞游戏升级为具有主动 AI 行为的即时战斗游戏。单位不再漫无目的地游荡,而是能够智能地寻找并追击敌人,大大提升了游戏的趣味性和策略深度。

追踪系统的实现涉及目标搜索、速度控制、物理协同等多个方面。核心算法简洁高效,能够在不影响帧率的情况下提供流畅的追踪体验。通过参数调节,系统提供了丰富的可定制性,开发者可以根据游戏设计需求调整追踪行为的激进程度和战斗节奏。

未来的优化方向包括空间分区加速、多目标策略、预测性追踪和协作追踪等。这些优化不仅能提升性能,还能带来更丰富的游戏策略可能性。希望本文的分析和代码解析能够帮助开发者理解追踪系统的设计思路,并在自己的项目中实现类似的功能。

欢迎加入开源鸿蒙PC社区:
https://harmonypc.csdn.net/

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐