【Cocos Creator】幽灵射手项目讲义 - 第三章:战斗系统核心模块

🎯 幽灵射手系列 · 全目录

文章目录

      • [🎯 幽灵射手系列 · 全目录](#🎯 幽灵射手系列 · 全目录)
    • 概述
    • [3.1 GameManager(游戏管理)](#3.1 GameManager(游戏管理))
      • [3.1.1 功能定位](#3.1.1 功能定位)
      • [3.1.2 静态属性(全局访问)](#3.1.2 静态属性(全局访问))
      • [3.1.3 核心方法](#3.1.3 核心方法)
        • [3.1.3.1 游戏初始化](#3.1.3.1 游戏初始化)
        • [3.1.3.2 关卡刷新](#3.1.3.2 关卡刷新)
        • [3.1.3.3 创建玩家](#3.1.3.3 创建玩家)
        • [3.1.3.4 怪物管理](#3.1.3.4 怪物管理)
        • [3.1.3.5 资源回收](#3.1.3.5 资源回收)
        • [3.1.3.6 游戏状态控制](#3.1.3.6 游戏状态控制)
    • [3.2 Player(玩家控制)](#3.2 Player(玩家控制))
      • [3.2.1 组件结构](#3.2.1 组件结构)
      • [3.2.2 初始化逻辑](#3.2.2 初始化逻辑)
      • [3.2.3 技能解析](#3.2.3 技能解析)
      • [3.2.4 攻击逻辑](#3.2.4 攻击逻辑)
      • [3.2.5 移动控制](#3.2.5 移动控制)
      • [3.2.6 血量管理](#3.2.6 血量管理)
    • [3.3 Monster(怪物控制)](#3.3 Monster(怪物控制))
      • [3.3.1 组件结构](#3.3.1 组件结构)
      • [3.3.2 初始化逻辑](#3.3.2 初始化逻辑)
      • [3.3.3 AI逻辑](#3.3.3 AI逻辑)
      • [3.3.4 攻击逻辑](#3.3.4 攻击逻辑)
      • [3.3.5 受击与死亡处理](#3.3.5 受击与死亡处理)
    • [3.4 MapManager(地图管理)](#3.4 MapManager(地图管理))
      • [3.4.1 功能定位](#3.4.1 功能定位)
      • [3.4.2 核心数据结构](#3.4.2 核心数据结构)
      • [3.4.3 地图构建流程](#3.4.3 地图构建流程)
      • [3.4.4 传送门管理](#3.4.4 传送门管理)
    • [3.5 Arrow(箭矢控制)](#3.5 Arrow(箭矢控制))
      • [3.5.1 组件结构](#3.5.1 组件结构)
      • [3.5.2 核心逻辑](#3.5.2 核心逻辑)
    • [3.6 战斗流程总结](#3.6 战斗流程总结)
    • 总结

概述

战斗系统是游戏的核心玩法部分,包括玩家控制、怪物AI、地图管理、箭矢系统等。本章详细讲解战斗系统的设计和实现。


3.1 GameManager(游戏管理)

3.1.1 功能定位

GameManager 是战斗系统的核心控制器,负责:

  • 游戏状态管理(开始/暂停/结束)
  • 关卡加载和切换
  • 玩家和怪物的全局管理
  • 资源回收和清理

3.1.2 静态属性(全局访问)

typescript 复制代码
// 游戏状态
public static isGameStart: boolean = false;    // 是否开始
public static isGamePause: boolean = false;    // 是否暂停
public static isGameOver: boolean = false;     // 是否结束
public static isWin: boolean = false;          // 是否胜利

// 场景节点
public static ndPlayer: Node;                  // 玩家节点
public static ndBoss: Node;                    // Boss节点
public static arrMonster: Node[] = [];         // 怪物数组
public static ndGameManager: Node;             // 游戏管理器节点

// 属性加成
public static attackAddition: number = 1;      // 敌人攻击加成
public static hpAddition: number = 1;          // 敌人生命加成
public static defenseAddition: number = 1;     // 敌人防御加成

// 调试模式
public static isTesting: boolean = true;       // 是否调试模式

3.1.3 核心方法

3.1.3.1 游戏初始化
typescript 复制代码
private _onGameInit(finishCb: Function) {
    // 获取当前关卡配置
    let level = PlayerData.instance.playerInfo.level;
    this.mapInfo = LocalConfig.instance.queryByID("checkpoint", String(level));
    
    // 设置敌人属性加成
    GameManager.attackAddition = this.mapInfo.attackAddition;
    GameManager.hpAddition = this.mapInfo.hpAddition;
    GameManager.defenseAddition = this.mapInfo.defenseAddition;
    
    // 重置游戏状态
    GameManager.isGameStart = false;
    GameManager.isGamePause = false;
    GameManager.isGameOver = false;
    GameManager.isWin = false;
    
    // 清理怪物数组
    GameManager.arrMonster = [];
    GameManager.ndBoss = null;
    GameManager.scriptBoss = null;
    
    // 显示加载界面并刷新关卡
    UIManager.instance.showDialog("loading/loadingPanel", [], () => {
        this._refreshLevel();
        finishCb && finishCb();
    });
}
3.1.3.2 关卡刷新
typescript 复制代码
private _refreshLevel() {
    // 回收所有资源
    this._recycleAll();
    
    // 获取关卡配置
    let level = PlayerData.instance.playerInfo.level;
    let mapName = LocalConfig.instance.queryByID("checkpoint", String(level)).mapName;
    let arrMap = mapName.split("#");
    
    // 设置当前地图索引
    this.curMapIndex = 0;
    this.totalMapNum = arrMap.length;
    
    // 加载第一个地图
    this._loadMap(arrMap[0], () => {
        // 地图加载完成
        UIManager.instance.hideDialog("loading/loadingPanel");
        
        // 创建玩家
        this._createPlayer();
        
        // 显示战斗界面
        UIManager.instance.showDialog("fight/fightPanel");
    });
}

private _loadMap(mapName: string, completeCb: Function) {
    // 构建地图
    this.scriptMapManager.buildMap(mapName, 
        (progress: number) => {
            // 更新加载进度
            let panel = this._dictPanel.get("loading/loadingPanel");
            if (panel && panel.scriptLoading) {
                panel.scriptLoading.setProgress(progress);
            }
        }, 
        () => {
            // 地图构建完成
            completeCb && completeCb();
        }
    );
}
3.1.3.3 创建玩家
typescript 复制代码
private _createPlayer() {
    ResourceUtil.loadModelRes(`player/${Constant.PLAYER_RES_NAME}`).then((prefab: Prefab) => {
        GameManager.ndPlayer = instantiate(prefab);
        
        // 设置初始位置(根据地图配置)
        let playerInfo = LocalConfig.instance.queryByID("base", Constant.PLAYER_ID);
        let pos = Util.parseVector3(playerInfo.position);
        GameManager.ndPlayer.setPosition(pos);
        
        // 添加到场景
        this.node.addChild(GameManager.ndPlayer);
        
        // 获取玩家脚本并初始化
        GameManager.scriptPlayer = GameManager.ndPlayer.getComponent(Player);
        GameManager.scriptPlayer.init();
        
        // 显示玩家血条
        UIManager.instance.showPlayerBloodBar(
            GameManager.scriptPlayer,
            GameManager.scriptPlayer.playerBaseInfo.hp,
            GameManager.scriptPlayer.curHp
        );
    });
}
3.1.3.4 怪物管理
typescript 复制代码
/**
 * 获取最近的怪物
 */
public static getNearestMonster(ndSelf: Node): Node {
    let minLength = Number.MAX_VALUE;
    let ndTarget: Node = null;
    
    for (let ndMonster of GameManager.arrMonster) {
        if (!ndMonster || !isValid(ndMonster)) continue;
        
        let scriptMonster = ndMonster.getComponent(Monster);
        if (!scriptMonster || scriptMonster.isDie) continue;
        
        let length = Util.getTwoNodeXZLength(ndSelf, ndMonster);
        if (length < minLength) {
            minLength = length;
            ndTarget = ndMonster;
        }
    }
    
    return ndTarget;
}

/**
 * 获取附近的怪物(用于闪电链等技能)
 */
public static getNearbyMonster(ndSelf: Node, maxDistance: number): Node[] {
    let arrResult: Node[] = [];
    
    for (let ndMonster of GameManager.arrMonster) {
        if (!ndMonster || !isValid(ndMonster)) continue;
        
        let scriptMonster = ndMonster.getComponent(Monster);
        if (!scriptMonster || scriptMonster.isDie) continue;
        
        let length = Util.getTwoNodeXZLength(ndSelf, ndMonster);
        if (length <= maxDistance * maxDistance) {
            arrResult.push(ndMonster);
        }
    }
    
    return arrResult;
}
3.1.3.5 资源回收
typescript 复制代码
private _recycleAll() {
    // 回收玩家
    if (GameManager.ndPlayer && isValid(GameManager.ndPlayer)) {
        PoolManager.instance.putNode(GameManager.ndPlayer);
        GameManager.ndPlayer = null;
        GameManager.scriptPlayer = null;
    }
    
    // 回收所有怪物
    for (let ndMonster of GameManager.arrMonster) {
        if (ndMonster && isValid(ndMonster)) {
            PoolManager.instance.putNode(ndMonster);
        }
    }
    GameManager.arrMonster = [];
    GameManager.ndBoss = null;
    GameManager.scriptBoss = null;
    
    // 回收地图
    if (this.scriptMapManager) {
        this.scriptMapManager.recycle();
    }
    
    // 回收特效和UI
    EffectManager.instance.recycleAll();
    UIManager.instance.hideDialog("fight/fightPanel");
}
3.1.3.6 游戏状态控制
typescript 复制代码
/**
 * 开始游戏
 */
public startGame() {
    GameManager.isGameStart = true;
    GameManager.isGamePause = false;
    
    // 开始玩家自动攻击
    GameManager.scriptPlayer.startAttack();
    
    // 开始怪物AI
    for (let ndMonster of GameManager.arrMonster) {
        let script = ndMonster.getComponent(Monster);
        if (script) {
            script.startAI();
        }
    }
}

/**
 * 暂停游戏
 */
public pauseGame() {
    GameManager.isGamePause = true;
    
    // 暂停玩家攻击
    GameManager.scriptPlayer.pauseAttack();
    
    // 暂停怪物AI
    for (let ndMonster of GameManager.arrMonster) {
        let script = ndMonster.getComponent(Monster);
        if (script) {
            script.pauseAI();
        }
    }
    
    // 显示暂停界面
    UIManager.instance.showDialog("pause/pausePanel");
}

/**
 * 继续游戏
 */
public resumeGame() {
    GameManager.isGamePause = false;
    
    // 继续玩家攻击
    GameManager.scriptPlayer.resumeAttack();
    
    // 继续怪物AI
    for (let ndMonster of GameManager.arrMonster) {
        let script = ndMonster.getComponent(Monster);
        if (script) {
            script.resumeAI();
        }
    }
    
    // 隐藏暂停界面
    UIManager.instance.hideDialog("pause/pausePanel");
}

/**
 * 游戏结束
 */
public gameOver(isWin: boolean) {
    GameManager.isGameOver = true;
    GameManager.isWin = isWin;
    GameManager.isGameStart = false;
    
    // 停止玩家攻击
    GameManager.scriptPlayer.stopAttack();
    
    // 停止怪物AI
    for (let ndMonster of GameManager.arrMonster) {
        let script = ndMonster.getComponent(Monster);
        if (script) {
            script.stopAI();
        }
    }
    
    // 分发游戏结束事件
    ClientEvent.dispatchEvent(Constant.EVENT_TYPE.ON_GAME_OVER, isWin);
    
    // 显示结算界面
    UIManager.instance.showDialog("settlement/settlementPanel", [isWin]);
}

3.2 Player(玩家控制)

3.2.1 组件结构

复制代码
Player (脚本)
    ├─ 引用组件:
    │   ├─ scriptPlayerModel: PlayerModel    // 模型控制
    │   └─ rigidBody: CharacterRigid         // 物理控制
    │
    ├─ 属性:
    │   ├─ playerBaseInfo                    // 基础配置
    │   ├─ curAttackPower                    // 当前攻击力
    │   ├─ curDefensePower                   // 当前防御力
    │   ├─ curAttackSpeed                    // 当前攻击速度
    │   ├─ curMoveSpeed                      // 当前移动速度
    │   ├─ curHp                             // 当前血量
    │   ├─ curHpLimit                        // 生命上限
    │   └─ 技能状态标志(isArrowDouble等)
    │
    └─ 方法:
        ├─ init()                            // 初始化
        ├─ playAction()                      // 执行动作
        ├─ throwArrowToEnemy()               // 射箭
        ├─ addBlood()                        // 增加血量
        ├─ reduceBlood()                     // 减少血量
        └─ _parsePlayerSkill()               // 解析技能

3.2.2 初始化逻辑

typescript 复制代码
public init() {
    // 获取基础配置
    this.playerBaseInfo = LocalConfig.instance.queryByID("base", Constant.PLAYER_ID);
    
    // 初始化属性(应用数值技能效果)
    this._parsePlayerSkill();
    
    // 设置初始血量
    this.curHp = this.playerBaseInfo.hp;
    
    // 初始化模型
    this.scriptPlayerModel = this.node.getComponent(PlayerModel);
    this.scriptPlayerModel.init(this.playerBaseInfo);
    
    // 初始化物理
    this.rigidBody = this.node.getComponent(CharacterRigid);
    this.rigidBody.maxSpeed = this.curMoveSpeed;
    
    // 开始自动攻击
    this._startAttackSchedule();
    
    // 监听事件
    ClientEvent.on(Constant.EVENT_TYPE.PARSE_PLAYER_SKILL, this._parsePlayerSkill, this);
}

3.2.3 技能解析

typescript 复制代码
private _parsePlayerSkill(isCoverSkill: boolean = false) {
    let arrSkill = PlayerData.instance.playerInfo.arrSkill;
    
    // 分类技能
    let arrFormChangeSkill = arrSkill.filter(s => s.startsWith("1"));  // 形态技能
    let arrValueChangeSkill = arrSkill.filter(s => s.startsWith("2")); // 数值技能
    let arrBuffSkill = arrSkill.filter(s => s.startsWith("3"));        // Buff技能
    let arrTriggerSkill = arrSkill.filter(s => s.startsWith("4"));      // 触发技能
    
    // 保存技能数组(用于显示)
    this._arrFormChangeSkill = arrFormChangeSkill;
    this._arrValueChangeSkill = arrValueChangeSkill;
    this._arrBuffSkill = arrBuffSkill;
    this._arrTriggerSkill = arrTriggerSkill;
    
    // 设置技能标志
    this.isArrowDouble = arrFormChangeSkill.includes(Constant.PLAYER_SKILL.ARROW_DOUBLE);
    this.isArrowContinuous = arrFormChangeSkill.includes(Constant.PLAYER_SKILL.ARROW_CONTINUOUS);
    this.isArrowUmbrella = arrFormChangeSkill.includes(Constant.PLAYER_SKILL.ARROW_UMBRELLA);
    this.isArrowReverse = arrFormChangeSkill.includes(Constant.PLAYER_SKILL.ARROW_REVERSE);
    this.isArrowSide = arrFormChangeSkill.includes(Constant.PLAYER_SKILL.ARROW_SIDE);
    this.isArrowPenetrate = arrFormChangeSkill.includes(Constant.PLAYER_SKILL.ARROW_PENETRATE);
    
    // Buff技能
    this.isArrowIce = arrBuffSkill.includes(Constant.PLAYER_SKILL.ARROW_ICE);
    this.isArrowFire = arrBuffSkill.includes(Constant.PLAYER_SKILL.ARROW_FIRE);
    
    // 触发技能
    this.isBloodthirsty = arrTriggerSkill.includes(Constant.PLAYER_SKILL.BLOODTHIRSTY);
    this.isArrowLightning = arrTriggerSkill.includes(Constant.PLAYER_SKILL.ARROW_LIGHTNING);
    this.isArrowLaunch = arrTriggerSkill.includes(Constant.PLAYER_SKILL.ARROW_LAUNCH);
    
    // 应用数值技能效果
    this._applyValueSkill();
}

private _applyValueSkill() {
    // 攻击力提升
    let attackRate = this._getValueSkillRate(Constant.PLAYER_SKILL.RAISE_ATTACK_01)
        + this._getValueSkillRate(Constant.PLAYER_SKILL.RAISE_ATTACK_02);
    this.curAttackPower = this.playerBaseInfo.attackPower * (1 + attackRate);
    
    // 攻击速度提升
    let speedRate = this._getValueSkillRate(Constant.PLAYER_SKILL.RAISE_ATTACK_SPEED_01)
        + this._getValueSkillRate(Constant.PLAYER_SKILL.RAISE_ATTACK_SPEED_02);
    this.curAttackSpeed = this.playerBaseInfo.attackSpeed * (1 + speedRate);
    
    // 移动速度提升
    let moveRate = this._getValueSkillRate(Constant.PLAYER_SKILL.MOVE_SPEED);
    this.curMoveSpeed = this.playerBaseInfo.moveSpeed * (1 + moveRate);
    
    // 生命上限提升
    let hpRate = this._getValueSkillRate(Constant.PLAYER_SKILL.RAISE_HP_LIMIT);
    this.curHpLimit = this.playerBaseInfo.hp * (1 + hpRate);
    
    // 更新物理组件速度
    if (this.rigidBody) {
        this.rigidBody.maxSpeed = this.curMoveSpeed;
    }
    
    // 更新攻击计时器
    this._updateAttackSchedule();
}

private _getValueSkillRate(skillId: string): number {
    let skill = LocalConfig.instance.queryByID("playerSkill", skillId);
    if (skill && this._arrValueChangeSkill.includes(skillId)) {
        return Number(skill.value);
    }
    return 0;
}

3.2.4 攻击逻辑

typescript 复制代码
private _startAttackSchedule() {
    // 计算攻击间隔(毫秒)
    let interval = 1000 / this.curAttackSpeed;
    
    // 设置攻击计时器
    this._attackSchedule = schedule(() => {
        if (GameManager.isGameStart && !GameManager.isGamePause && !this.isDie) {
            this._attackMonster();
        }
    }, interval / 1000);
}

private _updateAttackSchedule() {
    // 清除旧计时器
    if (this._attackSchedule) {
        unschedule(this._attackSchedule);
    }
    
    // 重新启动计时器
    this._startAttackSchedule();
}

private _attackMonster() {
    // 获取最近的怪物
    this._ndTarget = GameManager.getNearestMonster(this.node);
    
    if (!this._ndTarget || this.isDie) return;
    
    // 转向目标
    this._moveToTargetWorPos(this._ndTarget.worldPosition);
    
    // 播放攻击动画
    this.scriptPlayerModel.playAni(Constant.PLAYER_ANI_TYPE.ATTACK, false, () => {
        // 动画完成后自动射箭
        this.throwArrowToEnemy();
        
        // 如果不是连续攻击,继续寻找下一个目标
        if (!this.scriptPlayerModel.isRunning) {
            this._attackMonster();
        }
    });
}

public throwArrowToEnemy() {
    if (!this._ndTarget || !isValid(this._ndTarget)) return;
    
    // 设置朝向
    Vec3.subtract(this._forWard, this._ndTarget.worldPosition, this.node.worldPosition);
    this._forWard.y = 0;
    this._forWard.normalize();
    this.node.forward = this._forWard;
    
    // 根据技能状态选择箭矢类型
    let arrowType = this._getArrowType();
    
    // 创建箭矢
    ResourceUtil.loadModelRes(`weapon/arrow/${arrowType}`).then((prefab: Prefab) => {
        let ndArrow = PoolManager.instance.getNode(prefab, this.node.parent);
        
        // 设置箭矢初始位置(玩家前方)
        let arrowStartPos = new Vec3();
        Vec3.add(arrowStartPos, this.node.worldPosition, 
            new Vec3(this._forWard.x * 0.5, 0.2, this._forWard.z * 0.5));
        ndArrow.setWorldPosition(arrowStartPos);
        ndArrow.forward = this._forWard;
        
        // 初始化箭矢组件
        let scriptArrow = ndArrow.getComponent(Arrow);
        scriptArrow.init({
            speed: Constant.ARROW_SPEED,
            damage: this.curAttackPower,
            isPenetrate: this.isArrowPenetrate,
            isIce: this.isArrowIce,
            isFire: this.isArrowFire,
            isLightning: this.isArrowLightning,
            isLaunch: this.isArrowLaunch,
        });
    });
    
    // 播放射箭音效
    AudioManager.instance.playSound(Constant.SOUND_LOOSE);
}

private _getArrowType(): string {
    // 根据技能组合选择箭矢类型
    if (this.isArrowDouble) {
        if (this.isArrowContinuous) {
            return "arrowDoubleContinuous";
        }
        return "arrowDouble";
    }
    
    if (this.isArrowContinuous) {
        return "arrowSingleContinuous";
    }
    
    return "arrowSingle";
}

3.2.5 移动控制

typescript 复制代码
public playAction(obj: { x: number; z: number }) {
    if (this.isDie) return;
    
    // 设置移动方向
    let direction = new Vec3(obj.x, 0, obj.z);
    
    // 如果方向为0,停止移动
    if (direction.length() < 0.01) {
        this._stopMove();
        return;
    }
    
    // 归一化方向
    direction.normalize();
    
    // 设置物理速度
    this.rigidBody.setVelocity(direction.multiplyScalar(this.curMoveSpeed));
    
    // 转向移动方向
    this.node.forward = direction;
    
    // 播放跑步动画
    this.scriptPlayerModel.playRunAni(true);
}

private _stopMove() {
    // 停止物理移动
    this.rigidBody.setVelocity(Vec3.ZERO);
    
    // 播放待机动画
    this.scriptPlayerModel.playRunAni(false);
}

private _moveToTargetWorPos(targetPos: Vec3) {
    // 计算方向
    Vec3.subtract(this._offsetPos, targetPos, this.node.worldPosition);
    this._offsetPos.y = 0;
    
    // 如果距离小于攻击距离,不需要移动
    if (this._offsetPos.length() < this._attackDistance) {
        return;
    }
    
    // 设置朝向(不移动,只转向)
    this._offsetPos.normalize();
    this.node.forward = this._offsetPos;
}

3.2.6 血量管理

typescript 复制代码
public addBlood(num: number, isLimit: boolean = true) {
    let oldHp = this.curHp;
    
    if (isLimit) {
        this.curHp = Math.min(this.curHp + num, this.curHpLimit);
    } else {
        this.curHp += num;
    }
    
    // 通知UI更新
    ClientEvent.dispatchEvent(Constant.EVENT_TYPE.PLAYER_ADD_BLOOD, this.curHp - oldHp);
    
    // 更新血条
    if (this.scriptBloodBar) {
        this.scriptBloodBar.changeHp(this.curHp);
    }
}

public reduceBlood(baseInfo: any, isShowTips: boolean = true) {
    if (this.isDie) return;
    
    // 计算伤害(攻击力 - 防御力)
    let damage = Math.max(1, baseInfo.attackPower * GameManager.attackAddition - this.curDefensePower);
    
    // 暴击检测
    let isCritical = Math.random() < this.playerBaseInfo.criticalHitRate;
    if (isCritical) {
        damage *= this.playerBaseInfo.criticalHitDamage;
    }
    
    // 闪避检测
    let isDodge = Math.random() < this.playerBaseInfo.dodgeRate;
    if (isDodge) {
        // 显示闪避提示
        if (isShowTips) {
            UIManager.instance.showBloodTips(Constant.BLOOD_TIPS_TYPE.DODGE, 0, this.node.worldPosition);
        }
        return;
    }
    
    // 减少血量
    this.curHp -= damage;
    
    // 显示伤害提示
    if (isShowTips) {
        UIManager.instance.showBloodTips(
            isCritical ? Constant.BLOOD_TIPS_TYPE.CRITICAL_HIT : Constant.BLOOD_TIPS_TYPE.HIT_PLAYER,
            damage,
            this.node.worldPosition
        );
    }
    
    // 更新血条
    if (this.scriptBloodBar) {
        this.scriptBloodBar.changeHp(this.curHp);
    }
    
    // 播放受击动画
    this.scriptPlayerModel.playHitAni();
    
    // 播放受击音效
    AudioManager.instance.playSound(Constant.SOUND_HIT_PLAYER);
    
    // 检查是否死亡
    if (this.curHp <= 0) {
        this.showDie();
    }
}

public showDie() {
    if (this.isDie) return;
    
    this.isDie = true;
    GameManager.isGameStart = false;
    
    // 停止攻击
    this.stopAttack();
    
    // 播放死亡动画
    this.scriptPlayerModel.playAni(Constant.PLAYER_ANI_TYPE.DIE, false);
    
    // 播放死亡音效
    AudioManager.instance.playSound(Constant.SOUND_DIE);
    
    // 通知游戏结束
    ClientEvent.dispatchEvent(Constant.EVENT_TYPE.PLAYER_DIE);
}

3.3 Monster(怪物控制)

3.3.1 组件结构

复制代码
Monster (脚本)
    ├─ 引用组件:
    │   ├─ scriptMonsterModel: MonsterModel  // 模型控制
    │   └─ rigidBody: CharacterRigid         // 物理控制
    │
    ├─ 属性:
    │   ├─ baseInfo                          // 基础配置
    │   ├─ layerInfo                         // 关卡配置
    │   ├─ curHp                             // 当前血量
    │   ├─ curMoveSpeed                      // 当前移动速度
    │   ├─ curAttackSpeed                    // 当前攻击速度
    │   ├─ isDie                             // 是否死亡
    │   ├─ isMoving                          // 是否移动
    │   ├─ _movePattern                      // 移动模式
    │   ├─ _skillInfo                        // 当前技能
    │   └─ _allSkillInfo                     // 所有技能
    │
    └─ 方法:
        ├─ init()                            // 初始化
        ├─ playAction()                      // 执行动作
        ├─ playHit()                         // 受击处理
        ├─ releaseSkillToPlayer()            // 释放技能
        └─ showDie()                         // 死亡处理

3.3.2 初始化逻辑

typescript 复制代码
public init(baseInfo: any, layerInfo: any) {
    // 保存配置
    this.baseInfo = baseInfo;
    this.layerInfo = layerInfo;
    
    // 初始化属性(应用关卡加成)
    this.curHp = baseInfo.hp * GameManager.hpAddition;
    this.curMoveSpeed = baseInfo.moveSpeed * GameManager.moveSpeedAddition;
    this.curAttackSpeed = baseInfo.attackSpeed * GameManager.attackSpeedAddition;
    
    // 设置移动模式(优先使用关卡配置)
    this._movePattern = layerInfo.movePattern || baseInfo.movePattern;
    
    // 初始化技能
    this._initSkill();
    
    // 初始化模型
    this.scriptMonsterModel = this.node.getComponent(MonsterModel);
    this.scriptMonsterModel.init(baseInfo);
    
    // 初始化物理
    this.rigidBody = this.node.getComponent(CharacterRigid);
    this.rigidBody.maxSpeed = this.curMoveSpeed;
    
    // 显示血条(Boss和精英怪)
    if (baseInfo.type === Constant.BASE_TYPE.BOSS || this.curHp > 500) {
        UIManager.instance.showMonsterBloodBar(this, this.baseInfo.hp, GameManager.hpAddition);
    }
    
    // 开始AI
    this._startAI();
}

private _initSkill() {
    // 解析技能配置
    if (this.layerInfo.skill && this.layerInfo.skill !== "") {
        this._allSkillInfo = this.layerInfo.skill.split("#");
    } else {
        this._allSkillInfo = [];
    }
    
    // 选择初始技能
    if (this._allSkillInfo.length > 0) {
        this._skillIndex = 0;
        this._skillInfo = LocalConfig.instance.queryByID("monsterSkill", this._allSkillInfo[0]);
    }
}

3.3.3 AI逻辑

typescript 复制代码
private _startAI() {
    // 移动计时器
    this._moveSchedule = schedule(() => {
        if (GameManager.isGameStart && !GameManager.isGamePause && !this.isDie) {
            this._monsterMove();
        }
    }, 0.5);
    
    // 攻击计时器
    let attackInterval = 1000 / this.curAttackSpeed;
    this._attackSchedule = schedule(() => {
        if (GameManager.isGameStart && !GameManager.isGamePause && !this.isDie) {
            this._attackPlayer();
        }
    }, attackInterval / 1000);
}

private _monsterMove() {
    if (!GameManager.ndPlayer || !isValid(GameManager.ndPlayer)) return;
    
    switch (this._movePattern) {
        case Constant.MONSTER_MOVE_PATTERN.RANDOM:
            this._moveToRandomPos();
            break;
            
        case Constant.MONSTER_MOVE_PATTERN.FORWARD_PLAYER:
            this._moveToTargetWorPos(GameManager.ndPlayer.worldPosition);
            break;
            
        case Constant.MONSTER_MOVE_PATTERN.NO_MOVE:
            // 不移动,原地攻击
            break;
    }
}

private _moveToRandomPos() {
    // 随机生成目标位置
    let range = 5; // 移动范围
    let randomX = (Math.random() - 0.5) * range * 2;
    let randomZ = (Math.random() - 0.5) * range * 2;
    
    let targetPos = new Vec3(
        this._bornPos.x + randomX,
        this._bornPos.y,
        this._bornPos.z + randomZ
    );
    
    this._moveToTargetWorPos(targetPos);
}

private _moveToTargetWorPos(targetPos: Vec3) {
    Vec3.subtract(this._offsetPos, targetPos, this.node.worldPosition);
    this._offsetPos.y = 0;
    
    // 如果距离太近,不移动
    if (this._offsetPos.length() < 0.5) {
        if (this.isMoving) {
            this._stopMove();
        }
        return;
    }
    
    // 设置移动方向
    this._offsetPos.normalize();
    
    // 更新朝向
    this.node.forward = this._offsetPos;
    
    // 设置物理速度
    this.rigidBody.setVelocity(this._offsetPos.multiplyScalar(this.curMoveSpeed));
    
    // 播放跑步动画
    this.scriptMonsterModel.playRunAni(true);
    this.isMoving = true;
}

private _stopMove() {
    this.rigidBody.setVelocity(Vec3.ZERO);
    this.scriptMonsterModel.playRunAni(false);
    this.isMoving = false;
}

3.3.4 攻击逻辑

typescript 复制代码
private _attackPlayer() {
    if (!GameManager.ndPlayer || GameManager.scriptPlayer.isDie) return;
    
    // 计算与玩家的距离
    Vec3.subtract(this._offsetPos_2, GameManager.ndPlayer.worldPosition, this.node.worldPosition);
    this._offsetPos_2.y = 0;
    let distance = this._offsetPos_2.length();
    
    // 如果距离太远,不攻击
    if (distance > this.baseInfo.attackRange) {
        return;
    }
    
    // 转向玩家
    this._offsetPos_2.normalize();
    this.node.forward = this._offsetPos_2;
    
    // 停止移动
    this._stopMove();
    
    // 播放攻击动画
    this.scriptMonsterModel.playAni(Constant.MONSTER_ANI_TYPE.ATTACK, false, () => {
        // 动画完成后释放技能
        this.releaseSkillToPlayer();
        
        // 切换到下一个技能
        this._switchSkill();
    });
}

public releaseSkillToPlayer() {
    // 近战攻击(无技能时)
    if (!this._allSkillInfo.length) {
        let offsetLength = Util.getTwoNodeXZLength(this.node, GameManager.ndPlayer);
        if (offsetLength <= this.baseInfo.attackRange * this.baseInfo.attackRange) {
            GameManager.scriptPlayer.reduceBlood(this.baseInfo);
        }
        return;
    }
    
    // 远程技能攻击
    ResourceUtil.loadEffectRes(`${this._skillInfo.resName}/${this._skillInfo.resName}`).then((prefab: Prefab) => {
        let ndSkill = PoolManager.instance.getNode(prefab, GameManager.ndGameManager);
        
        // 设置技能位置和方向
        ndSkill.setWorldPosition(this.node.worldPosition);
        ndSkill.forward = this.node.forward;
        
        // 根据技能类型初始化
        let scriptSkill = ndSkill.getComponent(MonsterSkillBase);
        if (scriptSkill) {
            scriptSkill.init(this._skillInfo, this.baseInfo, this);
        }
    });
}

private _switchSkill() {
    // 循环切换技能
    if (this._allSkillInfo.length > 1) {
        this._skillIndex = (this._skillIndex + 1) % this._allSkillInfo.length;
        this._skillInfo = LocalConfig.instance.queryByID("monsterSkill", this._allSkillInfo[this._skillIndex]);
    }
}

3.3.5 受击与死亡处理

typescript 复制代码
public playHit(isArrowLaunch: boolean = false) {
    if (this.isDie) return;
    
    // 播放受击动画
    this.scriptMonsterModel.playHitAni();
    
    // 如果是弹射箭矢,击退怪物
    if (isArrowLaunch) {
        let direction = Vec3.subtract(new Vec3(), this.node.worldPosition, GameManager.ndPlayer.worldPosition);
        direction.y = 0;
        direction.normalize();
        
        this.rigidBody.setVelocity(direction.multiplyScalar(2));
        scheduleOnce(() => {
            this._stopMove();
        }, 0.3);
    }
}

public reduceBlood(damage: number, isShowTips: boolean = true) {
    if (this.isDie) return;
    
    this.curHp -= damage;
    
    // 显示伤害提示
    if (isShowTips) {
        UIManager.instance.showBloodTips(Constant.BLOOD_TIPS_TYPE.HIT_MONSTER, damage, this.node.worldPosition);
    }
    
    // 更新血条
    if (this.scriptBloodBar) {
        this.scriptBloodBar.changeHp(this.curHp);
    }
    
    // 检查是否死亡
    if (this.curHp <= 0) {
        this.showDie();
    }
}

public showDie() {
    if (this.isDie) return;
    
    this.isDie = true;
    
    // 停止AI
    this.stopAI();
    
    // 停止移动
    this._stopMove();
    
    // 播放死亡动画
    this.scriptMonsterModel.playAni(Constant.MONSTER_ANI_TYPE.DIE, false, () => {
        // 动画完成后回收
        this._recycleSelf();
    });
    
    // 播放死亡音效
    AudioManager.instance.playSound(Constant.SOUND_MONSTER_DIE);
    
    // 掉落奖励
    this._dropReward();
    
    // 从怪物数组中移除
    let idx = GameManager.arrMonster.indexOf(this.node);
    if (idx >= 0) {
        GameManager.arrMonster.splice(idx, 1);
    }
    
    // 检查是否胜利
    this._checkWin();
}

private _dropReward() {
    // 掉落金币
    let goldNum = Math.floor(this.baseInfo.goldNum * (0.8 + Math.random() * 0.4));
    if (goldNum > 0) {
        // 创建金币奖励
        ResourceUtil.loadModelRes("reward/gold").then((prefab: Prefab) => {
            let ndGold = PoolManager.instance.getNode(prefab, GameManager.ndGameManager);
            ndGold.setWorldPosition(this.node.worldPosition);
            
            let scriptGold = ndGold.getComponent(Gold);
            scriptGold.init(goldNum);
        });
        
        // 更新玩家金币
        PlayerData.instance.updatePlayerInfo('gold', goldNum);
    }
    
    // 掉落爱心(概率)
    if (Math.random() < this.baseInfo.heartDropRate) {
        ResourceUtil.loadModelRes("reward/heart").then((prefab: Prefab) => {
            let ndHeart = PoolManager.instance.getNode(prefab, GameManager.ndGameManager);
            ndHeart.setWorldPosition(this.node.worldPosition);
            
            let scriptHeart = ndHeart.getComponent(Heart);
            scriptHeart.init();
        });
    }
}

private _checkWin() {
    // 检查是否所有怪物都已死亡
    if (GameManager.arrMonster.length === 0) {
        GameManager.gameOver(true);
    }
}

private _recycleSelf() {
    // 回收血条
    if (this.scriptBloodBar) {
        PoolManager.instance.putNode(this.scriptBloodBar.node);
        this.scriptBloodBar = null;
    }
    
    // 回收怪物节点
    PoolManager.instance.putNode(this.node);
}

3.4 MapManager(地图管理)

3.4.1 功能定位

MapManager 负责加载和管理关卡地图,包括:

  • 解析地图配置文件
  • 创建怪物、NPC、道具等实体
  • 管理传送门(关卡切换)

3.4.2 核心数据结构

typescript 复制代码
private _dictModuleType: { [key: string]: any[] } = {};  // 按类型分组的实体配置
private _arrMap: any[];                                    // 地图配置数组

3.4.3 地图构建流程

typescript 复制代码
public buildMap(mapName: string, progressCb?: Function, completeCb?: Function) {
    // 清空之前的配置
    this._dictModuleType = {};
    
    // 获取地图配置
    this._arrMap = LocalConfig.instance.getTableArr(mapName);
    
    // 按类型分组
    for (let item of this._arrMap) {
        let baseInfo = LocalConfig.instance.queryByID("base", item.ID);
        if (!baseInfo) continue;
        
        let type = baseInfo.type;
        if (!this._dictModuleType[type]) {
            this._dictModuleType[type] = [];
        }
        this._dictModuleType[type].push(item);
    }
    
    // 批量创建各类型实体
    let arrPromise: Promise<void>[] = [];
    
    for (const type in this._dictModuleType) {
        if (this._dictModuleType[type].length > 0) {
            arrPromise.push(this._buildModel(type));
        }
    }
    
    // 等待所有实体创建完成
    Promise.all(arrPromise).then(() => {
        // 显示传送门(最后一关不显示)
        let level = PlayerData.instance.playerInfo.level;
        if (level < Constant.MAX_LEVEL) {
            this._showWarpGate();
        }
        
        progressCb && progressCb(1);
        completeCb && completeCb();
    });
}

private _buildModel(type: string): Promise<void> {
    return new Promise((resolve) => {
        let objItems = this._dictModuleType[type];
        let createCount = 0;
        
        for (let layerInfo of objItems) {
            let baseInfo = LocalConfig.instance.queryByID("base", layerInfo.ID);
            let modelPath = `${type}/${baseInfo.resName}`;
            
            ResourceUtil.loadModelRes(modelPath).then((prefab: Prefab) => {
                // 创建父节点(按类型分组)
                let parentName = type + 'Group';
                let ndParent = this.node.getChildByName(parentName);
                if (!ndParent) {
                    ndParent = new Node(parentName);
                    this.node.addChild(ndParent);
                }
                
                // 创建实体
                let ndChild = PoolManager.instance.getNode(prefab, ndParent);
                
                // 设置位置(优先使用关卡配置,否则使用基础配置)
                let pos = layerInfo.position ? 
                    Util.parseVector3(layerInfo.position) : 
                    Util.parseVector3(baseInfo.position);
                ndChild.setPosition(pos);
                
                // 设置角度
                let angle = layerInfo.angle ? 
                    Util.parseVector3(layerInfo.angle) : 
                    Util.parseVector3(baseInfo.angle);
                ndChild.eulerAngles = angle;
                
                // 设置缩放
                let scale = layerInfo.scale ? 
                    Util.parseVector3(layerInfo.scale) : 
                    Util.parseVector3(baseInfo.scale);
                ndChild.setScale(scale);
                
                // 保存出生位置(用于怪物随机移动)
                if (type === Constant.BASE_TYPE.MONSTER || type === Constant.BASE_TYPE.BOSS) {
                    ndChild.setUserData('bornPos', ndChild.worldPosition.clone());
                }
                
                // 初始化组件
                this._initComponent(ndChild, type, baseInfo, layerInfo);
                
                createCount++;
                if (createCount >= objItems.length) {
                    resolve();
                }
            });
        }
    });
}

private _initComponent(ndChild: Node, type: string, baseInfo: any, layerInfo: any) {
    switch (type) {
        case Constant.BASE_TYPE.MONSTER:
            let scriptMonster = ndChild.getComponent(Monster);
            scriptMonster.init(baseInfo, layerInfo);
            GameManager.arrMonster.push(ndChild);
            break;
            
        case Constant.BASE_TYPE.BOSS:
            let scriptBoss = ndChild.getComponent(Boss);
            scriptBoss.init(baseInfo, layerInfo);
            GameManager.arrMonster.push(ndChild);
            GameManager.ndBoss = ndChild;
            GameManager.scriptBoss = scriptBoss;
            break;
            
        case Constant.BASE_TYPE.HEART:
            let scriptHeart = ndChild.getComponent(Heart);
            scriptHeart.init();
            break;
            
        case Constant.BASE_TYPE.NPC:
            let scriptNPC = ndChild.getComponent(NPC);
            scriptNPC.init(baseInfo);
            break;
    }
}

3.4.4 传送门管理

typescript 复制代码
private _showWarpGate() {
    ResourceUtil.loadModelRes("map/warpGate").then((prefab: Prefab) => {
        let ndWarpGate = PoolManager.instance.getNode(prefab, this.node);
        
        // 设置传送门位置(地图中心偏右)
        ndWarpGate.setPosition(8, 0, 0);
        
        // 初始化传送门组件
        let scriptWarpGate = ndWarpGate.getComponent(WarpGate);
        scriptWarpGate.init();
        
        // 保存传送门引用
        this._ndWarpGate = ndWarpGate;
    });
}

public enterNextLevel() {
    // 隐藏传送门
    if (this._ndWarpGate && isValid(this._ndWarpGate)) {
        PoolManager.instance.putNode(this._ndWarpGate);
        this._ndWarpGate = null;
    }
    
    // 进入下一关
    let level = PlayerData.instance.playerInfo.level;
    let mapName = LocalConfig.instance.queryByID("checkpoint", String(level)).mapName;
    let arrMap = mapName.split("#");
    
    // 更新当前地图索引
    GameManager.scriptGameManager.curMapIndex++;
    
    // 如果还有地图,加载下一个
    if (GameManager.scriptGameManager.curMapIndex < arrMap.length) {
        UIManager.instance.showDialog("loading/loadingPanel", [], () => {
            this.recycle();
            this.buildMap(arrMap[GameManager.scriptGameManager.curMapIndex], () => {
                UIManager.instance.hideDialog("loading/loadingPanel");
            });
        });
    } else {
        // 所有地图完成,进入下一关
        PlayerData.instance.updatePlayerInfo('level', 1);
        PlayerData.instance.updatePlayerInfo('gold', 100); // 通关奖励
        
        // 重新初始化游戏
        ClientEvent.dispatchEvent(Constant.EVENT_TYPE.ON_GAME_INIT);
    }
}

public recycle() {
    // 回收所有子节点
    for (let i = this.node.children.length - 1; i >= 0; i--) {
        let child = this.node.children[i];
        if (child.name !== "mapGroup") { // 保留地图场景
            PoolManager.instance.putNode(child);
        }
    }
    
    // 清空分组
    this._dictModuleType = {};
}

3.5 Arrow(箭矢控制)

3.5.1 组件结构

复制代码
Arrow (脚本)
    ├─ 引用组件:
    │   └─ collider: ColliderComponent       // 碰撞组件
    │
    ├─ 属性:
    │   ├─ _speed                            // 飞行速度
    │   ├─ _damage                           // 伤害值
    │   ├─ _isPenetrate                      // 是否穿透
    │   ├─ _isIce                            // 是否冰冻
    │   ├─ _isFire                           // 是否灼烧
    │   ├─ _isLightning                      // 是否闪电
    │   └─ _isLaunch                         // 是否弹射
    │
    └─ 方法:
        ├─ init()                            // 初始化
        ├─ _update()                         // 更新飞行
        ├─ _onCollisionEnter()               // 碰撞检测
        └─ _destroyArrow()                   // 销毁箭矢

3.5.2 核心逻辑

typescript 复制代码
public init(data: {
    speed: number;
    damage: number;
    isPenetrate?: boolean;
    isIce?: boolean;
    isFire?: boolean;
    isLightning?: boolean;
    isLaunch?: boolean;
}) {
    // 设置属性
    this._speed = data.speed;
    this._damage = data.damage;
    this._isPenetrate = data.isPenetrate || false;
    this._isIce = data.isIce || false;
    this._isFire = data.isFire || false;
    this._isLightning = data.isLightning || false;
    this._isLaunch = data.isLaunch || false;
    
    // 设置飞行时间(防止无限飞行)
    this._flyTime = 0;
    
    // 启用碰撞检测
    this.colliderCom = this.node.getComponent(ColliderComponent);
    this.colliderCom.on('onCollisionEnter', this._onCollisionEnter, this);
    
    // 开始飞行
    this._isFlying = true;
}

protected update(deltaTime: number) {
    if (!this._isFlying) return;
    
    // 更新飞行时间
    this._flyTime += deltaTime;
    
    // 飞行超时检测
    if (this._flyTime > Constant.ARROW_MAX_FLY_TIME) {
        this._destroyArrow();
        return;
    }
    
    // 向前移动
    let moveDistance = this._speed * deltaTime;
    this.node.translate(new Vec3(0, 0, moveDistance));
}

private _onCollisionEnter(other: ColliderComponent) {
    let group = other.getGroup();
    
    // 击中怪物
    if (group === Constant.PHY_GROUP.MONSTER || group === Constant.PHY_GROUP.BOSS) {
        let scriptMonster = other.node.getComponent(Monster) || other.node.getComponent(Boss);
        
        // 造成伤害
        scriptMonster.reduceBlood(this._damage);
        
        // 应用技能效果
        this._applySkillEffect(scriptMonster);
        
        // 播放击中特效
        this._playHitEffect();
        
        // 如果不是穿透,销毁箭矢
        if (!this._isPenetrate) {
            this._destroyArrow();
            return;
        }
    }
    
    // 击中障碍物
    if (group === Constant.PHY_GROUP.OBSTACLE) {
        // 播放撞击特效
        this._playHitEffect();
        
        if (!this._isPenetrate) {
            this._destroyArrow();
        }
    }
    
    // 击中玩家(箭矢反弹)
    if (group === Constant.PHY_GROUP.PLAYER) {
        // 玩家箭矢不应击中自己
        return;
    }
}

private _applySkillEffect(scriptMonster: Monster | Boss) {
    // 冰冻效果
    if (this._isIce) {
        scriptMonster.addBuff(Constant.BUFF_TYPE.ICE, 2);
    }
    
    // 灼烧效果
    if (this._isFire) {
        scriptMonster.addBuff(Constant.BUFF_TYPE.FIRE, 3);
    }
    
    // 闪电链效果
    if (this._isLightning) {
        this._triggerLightning(scriptMonster);
    }
    
    // 弹射效果(已在Monster.playHit中处理)
}

private _triggerLightning(scriptMonster: Monster | Boss) {
    // 获取附近的怪物
    let arrNearby = GameManager.getNearbyMonster(scriptMonster.node, 5);
    
    // 对每个附近怪物造成伤害
    for (let ndMonster of arrNearby) {
        if (ndMonster === scriptMonster.node) continue;
        
        let targetScript = ndMonster.getComponent(Monster) || ndMonster.getComponent(Boss);
        if (targetScript && !targetScript.isDie) {
            // 闪电伤害为原伤害的50%
            targetScript.reduceBlood(this._damage * 0.5);
            
            // 播放闪电特效
            EffectManager.instance.showLightning(scriptMonster.node.worldPosition, ndMonster.worldPosition);
        }
    }
}

private _playHitEffect() {
    // 播放击中特效
    ResourceUtil.loadEffectRes("hit/hit").then((prefab: Prefab) => {
        let ndEffect = PoolManager.instance.getNode(prefab, this.node.parent);
        ndEffect.setWorldPosition(this.node.worldPosition);
        
        // 3秒后回收特效
        scheduleOnce(() => {
            PoolManager.instance.putNode(ndEffect);
        }, 3);
    });
}

private _destroyArrow() {
    this._isFlying = false;
    
    // 移除碰撞监听
    this.colliderCom.off('onCollisionEnter', this._onCollisionEnter, this);
    
    // 回收箭矢
    PoolManager.instance.putNode(this.node);
}

3.6 战斗流程总结

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                       战斗流程                                  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                    游戏初始化 (_onGameInit)
                              │
                              ▼
                    加载关卡配置 (checkpoint.csv)
                              │
                              ▼
                    设置敌人属性加成
                              │
                              ▼
               ┌───────────────┴───────────────┐
               ▼                               ▼
        创建玩家节点                       构建地图 (MapManager)
               │                               │
               ▼                               ▼
        初始化玩家属性                     创建怪物/NPC/道具
               │                               │
               └───────────────┬───────────────┘
                               ▼
                         开始游戏 (startGame)
                               │
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
       玩家自动攻击        怪物AI启动        显示战斗UI
              │                │
              ▼                ▼
       射箭 → 碰撞检测    移动 → 攻击 → 技能释放
              │                │
              ▼                ▼
       怪物受伤/死亡     玩家受伤/死亡
              │                │
              ▼                ▼
       掉落奖励           游戏结束判断
              │                │
              └────────────────┼────────────────┘
                              ▼
                    检查胜利条件(所有怪物死亡)
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
           胜利                           失败
              │                               │
              ▼                               ▼
       进入下一关                       显示复活界面

总结

战斗系统是游戏的核心玩法,主要包含:

  1. GameManager:全局游戏状态管理
  2. Player:玩家角色控制(移动、攻击、技能)
  3. Monster:怪物AI控制(移动、攻击、技能)
  4. MapManager:地图加载和实体管理
  5. Arrow:箭矢飞行和碰撞检测

这些模块协同工作,实现了完整的战斗逻辑。理解战斗系统对于扩展游戏玩法至关重要。

相关推荐
Amctwd12 小时前
【Cocos Creator】幽灵射手项目讲义 - 第二章:框架层核心模块
cocos
Amctwd16 小时前
【Cocos Creator】幽灵射手项目讲义 - 第一章:项目概述
cocos
LcGero12 天前
Cocos Creator 热更新地址动态化方案
cocos·动态化·热更
weixin_409383122 个月前
cocosshader像素风沙消散
shader·cocos
weixin_409383123 个月前
cocos shader消失
shader·cocos
weixin_409383123 个月前
cocos魔法阵shader
shader·cocos
weixin_409383123 个月前
cocos抛物线掉落装备 游戏中的抛物线应用x²=-2py 开口向下
游戏·cocos·抛物线
weixin_409383123 个月前
cocos 按钮光环shader
shader·cocos
weixin_409383124 个月前
a星学习记录 通过父节点从目的地格子坐标回溯起点
学习·cocos·a星