2026-03-28 16:15:28 +08:00

21 KiB
Raw Blame History

name, description, user-invocable
name description user-invocable
th1-logic 用于 TH1 回合制策略游戏逻辑开发。使用场景:新增技能(Skill)、行为(Action)、数据(Data)、逻辑(Logic)的标准开发流程。触发词技能、action、skill、game logic、th1 true

TH1_Logic 游戏逻辑开发 Skill

用于 TH1 回合制策略游戏的逻辑层开发,包括:新增技能(Skill)、行为(Action)、数据(Data)、逻辑(Logic) 的标准开发流程。


架构概览

TH1_Data/          ← 纯数据层 (namespace RuntimeData)
TH1_Logic/
  ├── Action/      ← 行为系统 (namespace Logic.Action)
  ├── Skill/       ← 技能系统 (namespace Logic.Skill)
  ├── Unit/        ← 单位逻辑 (namespace Logic)
  ├── Player/      ← 玩家逻辑 (namespace Logic)
  └── City/        ← 城市逻辑 (namespace Logic)

核心设计原则

  1. 数据与逻辑分离RuntimeData 命名空间放纯数据,逻辑通过 Main.UnitLogicMain.PlayerLogicMain.CityLogic 静态入口访问
  2. MapData 是中心枢纽:所有跨实体查询都通过 MapData 中转(如 mapData.GetPlayerDataByUnitId()mapData.GetGridDataByUnitId()
  3. IdentifierBase 四大实体PlayerDataUnitDataCityDataGridData 均继承 IdentifierBase,拥有 uint IdList<SkillBase> Skills
  4. 逻辑-视觉分离:逻辑操作不直接操控视觉,通过 Renderer(mapData)?.InstantUpdateXxx() 间接更新
  5. 序列化兼容:数据类使用 [MemoryPackable],必须提供 [MemoryPackConstructor],私有字段需 [MemoryPackInclude],非序列化字段用 [MemoryPackIgnore]

一、新增技能 (Skill)

步骤

Step 1SkillType 枚举中注册

SkillBase.csSkillType 枚举末尾(Max = 999 之前)新增枚举项。只能新增,不能删除已有项

// SkillBase.cs 中的 SkillType 枚举
public enum SkillType
{
    // ... 已有技能 ...
    NewSkillName = <下一个可用数字>,
    Max = 999,
}

Step 2创建技能类文件

TH1_Logic/Skill/AllSkill/ 目录下创建 NewSkillNameSkill.cs

using RuntimeData;
using MemoryPack;

namespace Logic.Skill
{
    public partial class NewSkillNameSkill : SkillBase
    {
        public NewSkillNameSkill()
        {
            IsPermanent = true;      // true=永久技能, false=有回合限制
            TurnsLimit = 0;          // 如果非永久,设置持续回合数
            Score = 2;               // AI 评估用的分数权重
            // IsLevelSkill = false; // 是否为叠层技能
            // _autoDisappear = true; // 叠层为0时是否自动消失
        }

        public override SkillType GetSkillType()
        {
            return SkillType.NewSkillName;
        }

        // === 根据需求选择性重写以下生命周期/判断方法 ===
    }
}

Step 3选择性重写方法

根据技能功能,从以下分类中选择需要重写的方法:

生命周期类(事件触发时调用):

方法 触发时机 典型场景
OnTurnStart(IdentifierBase self, MapData map) 回合开始 自动治疗、回合buff
OnAfterTurnStart(IdentifierBase self, MapData map) 回合开始之后 避免技能时序冲突
OnTurnEnd(IdentifierBase self, MapData map) 回合结束 回合结束效果
OnMove(UnitData self, GridData grid, MapData map, MoveType moveType, ...) 移动后 冲锋(Dash)、移动触发技能
BeforeMove(UnitData self, GridData grid, MapData map, MoveType moveType, ...) 移动前 移动前判断
BeforeDamagedSupportStage(MapData map, SettlementInfo info) 受伤结算前(支援阶段) 承伤减免
BeforeDamagedTransformStage(MapData map, SettlementInfo info) 受伤结算前(转移阶段) 伤害转移
OnDamaged(MapData map, SettlementInfo info) 受伤结算时 受伤反应
BeforeDamageOther(MapData map, SettlementInfo info) 对他人伤害结算前 额外伤害
OnDamageOther(MapData map, SettlementInfo info) 对他人伤害结算时 击杀/转化效果
AfterDamageOther(MapData map, SettlementInfo info) 对他人伤害结算后 击后效果
BeforeActiveAttackOther(MapData map, UnitData origin, UnitData target, out int addDmg) 主动攻击之前 额外伤害加成
AfterActiveAttackOther(MapData map, UnitData origin, UnitData target) 主动攻击之后 攻击后效果
OnSkillAdd(MapData map, uint originId) 技能被添加时 初始化效果
OnSkillOverride(MapData map, uint originId, uint selfId) 技能被覆盖时 叠层技能增层
OnFinished(IdentifierBase self, MapData map) 技能结束(超时/移除)时 清理效果

全局事件类(附着在任意实体上,监听全局事件):

方法 触发时机
OnAnyUnitMove(MapData, IdentifierBase, UnitData, GridData, MoveType) 任何单位移动时
OnAnyUnitDie(MapData, UnitData self, UnitData dieUnit) 任何单位死亡时
OnAnyUnitCreate(MapData, IdentifierBase, UnitData newUnit) 任何单位创建时
BeforeUnitDamaged(UnitData self, MapData, SettlementInfo) 任何单位受伤前
OnUnitDamaged(UnitData self, MapData, SettlementInfo) 任何单位受伤时

判断属性类(影响单位能力判定):

方法 返回值 用途
IsLimitSelfMove(UnitData, MapData) bool 限制自身移动
IsLimitSelfAttack(UnitData, MapData) bool 限制自身攻击
IsLimitTargetCounterAttack(UnitData, MapData) bool 限制敌方反击
IsLimitSelfCounterAttack(UnitData, MapData) bool 限制自身反击
IsIgnoreControlArea(UnitData, MapData) bool 无视敌人控制区
IsCanMoveOnTerrain(UnitData, MapData, TerrainType) bool 能在指定地形移动
IsCanMoveOnFeature(UnitData, MapData, TerrainFeature) bool 能在指定地貌移动
IsIgnoreMoveLoss(UnitData, MapData, TerrainFeature/Vegetation) bool 无视地形移动惩罚
IsInvisible(UnitData, MapData) bool 隐身状态
CanAttackAll(UnitData, MapData) bool 能攻击所有人
IsCanBeKill(UnitData, MapData) bool 能否被击杀

数值修正类(影响战斗计算):

方法 返回值 用途
GetAttackAdditionParam(MapData, UnitData, UnitData) float 攻击加法修正
GetAttackMultiplicationParam(MapData, UnitData, UnitData) float 攻击乘法修正 (1.0=无修正)
GetDefenseAdditionParam(MapData, UnitData, UnitData) float 防御加法修正
GetDefenseMultiplicationParam(MapData, UnitData, UnitData) float 防御乘法修正
GetExtraSight(UnitData, MapData) int 额外视野范围
GetExtraMoveRange(MapData, UnitData) int 额外移动范围
GetExtraAttackRange(MapData, UnitData) int 额外攻击范围
GetCriticalHitRate(UnitData, MapData) float 暴击率

变形保留判断

方法 说明
ReservedOnTransform(UnitData self, UnitFullType fullType) 单位类型变换时是否保留此技能
ReservedOnTransformBoat(UnitData, UnitFullType) 陆→船变换时保留
ReservedOnTransformFromBoat(UnitData, UnitFullType) 船→陆变换时保留
ReservedOnTransformUpgrade(UnitData, UnitFullType) 升级变换时保留

Step 4配置技能数据

在表格配置 SkillDataAssets 中注册新技能的 skillPriority 等信息。

Step 5挂载技能

技能通过 IdentifierBase 的方法挂载到实体:

// 标准添加
entity.AddOrOverrideSkill(SkillType.NewSkillName, mapData, originId);

// 初始化添加(新建单位时)
entity.AddInitSkill(SkillType.NewSkillName, mapData);

// Legacy 添加(兼容旧逻辑,含回合限制/叠层/强制覆盖)
entity.AddSkill_Legacy(SkillType.NewSkillName, mapData, 
    IsPermanent, turnLimit, IsLevel, level, autoDisappear, 
    SpecialAddSkillType.Normal, originId);

技能示例模板

简单属性技能攻击力翻倍1回合

public partial class AttackUpSkill : SkillBase
{
    public AttackUpSkill()
    {
        IsPermanent = false;
        TurnsLimit = 1;
        Score = 2;
    }
    public override SkillType GetSkillType() => SkillType.ATTACKUP;
    public override float GetAttackMultiplicationParam(MapData mapData, UnitData self, UnitData target = null) => 1.5f;
    public override bool ReservedOnTransform(UnitData self, UnitFullType fullType) => true;
}

生命周期技能(回合开始治疗周围友军)

public partial class AutoHealSkill : SkillBase
{
    public AutoHealSkill()
    {
        IsPermanent = true;
        TurnsLimit = 0;
        Score = 2;
    }
    public override SkillType GetSkillType() => SkillType.AUTOHEAL;
    public override void OnTurnStart(IdentifierBase identifier, MapData mapData)
    {
        var self = identifier as UnitData;
        if (self == null) return;
        var player = self.Player(mapData);
        if (player == null) return;
        var selfUnitList = new HashSet<UnitData>();
        mapData.GetUnitDataListByPlayerId(player.Id, selfUnitList);
        if (!mapData.GetGridDataByUnitId(self.Id, out var targetGrid)) return;
        var roundGrid = mapData.GridMap.GetAroundGridData(1, 1, targetGrid);
        foreach (var grid in roundGrid)
        {
            if (!mapData.GetUnitDataByGid(grid.Id, out var unit)) continue;
            if (!selfUnitList.Contains(unit)) continue;
            unit.Health += 2;
            if (unit.Health > unit.GetMaxHealth())
                unit.Health = unit.GetMaxHealth();
        }
    }
}

事件触发技能(移动后保留攻击力)

public partial class DashSkill : SkillBase
{
    public bool IsTrigger = false;
    public override void OnRefresh() { IsTrigger = false; }
    public override void OnTurnStart(IdentifierBase identifier, MapData mapData) { IsTrigger = false; }
    public DashSkill() { IsPermanent = true; Score = 4; }
    public override SkillType GetSkillType() => SkillType.DASH;
    public override void OnDamageOther(MapData mapData, SettlementInfo info)
    {
        if (info.DamageType != DamageType.ActiveAttack) return;
        IsTrigger = true;
    }
    public override void OnMove(UnitData self, GridData grid, MapData mapData, MoveType moveType, List<Vector2Int> path = null)
    {
        if (IsTrigger) return;
        if (moveType == MoveType.ActiveMove) self.AP = 1;
    }
}

二、新增行为 (Action)

行为类型

ActionType 说明 基类 操作主体
Build 建造建筑 BuildAction Grid
TrainUnit 训练单位 TrainUnitAction City/Grid
UnitAction 单位行动 UnitActionAction Unit
GridMisc 格子杂项 GridMiscAction Grid
PlayerAction 玩家行动 PlayerActionAction Player
UnitMove 单位移动 UnitMoveAction Unit
UnitAttack 单位攻击 UnitAttackAction Unit

标准开发流程

Step 1注册行为子类型枚举如需

在对应枚举中新增,如 UnitActionTypeGridMiscActionTypePlayerActionType

Step 2创建或扩展行为类

每个行为必须实现三个核心方法:

public class NewAction : ActionLogicBase
{
    public NewAction(CommonActionId id) : base(id) { }

    // 执行行为逻辑
    protected override bool Execute(CommonActionParams actionParams)
    {
        // Step 1: 鲁棒性检查
        if (!CheckCan(actionParams)) return false;
        
        // Step 2: 扣费/消耗
        actionParams.PlayerData.SpendCoin(GetCost(actionParams));
        
        // Step 3: 核心逻辑修改 MapData
        // ...
        
        // Step 4: 视觉更新
        // grid.Renderer(mapData)?.InstantUpdateGrid(true);
        // unit.Renderer(mapData)?.InstantUpdateUnit(true);
        
        // Step 5: 后续逻辑(更新连通性、建筑等级等)
        // Main.PlayerLogic.UpdateCityConnect(mapData, playerData);
        // Main.PlayerLogic.UpdateTerritoryAllBuildingLevel(mapData, playerId);
        
        return true;
    }

    // 判断能否执行逻辑严格判断用于AI和玩家操作
    public override bool CheckCan(CommonActionParams actionParam)
    {
        // 1. 鲁棒性MainObjectType检查
        // 2. 科技检查playerData.TechTree.CheckActionCan(_actionId)
        // 3. 资源检查GetCost() <= playerData.PlayerCoin
        // 4. 领土检查:是否在自己领土
        // 5. 敌人检查:格子上有无敌人
        // 6. 特殊条件
        return true;
    }

    // 判断是否显示在UI中比CheckCan宽松不检查金钱
    public override bool CheckShow(CommonActionParams actionParam, out ShowType showType)
    {
        showType = ShowType.None;
        // 类似CheckCan但不检查金钱
        return true;
    }
}

Step 3注册到工厂

ActionLogicFactory 中注册新行为,使 GetActionLogic() 能通过 CommonActionId 创建实例。

Step 4构建调用参数

var actionId = new CommonActionId 
{ 
    ActionType = CommonActionType.UnitAction,
    UnitActionType = UnitActionType.NewAction 
};
var action = ActionLogicFactory.GetActionLogic(actionId);
var param = new CommonActionParams(
    mapData: mapData,
    playerData: playerData,
    unitData: unitData,
    gridData: gridData,
    mainObjectType: MainObjectType.Unit
);
action?.CompleteExecute(param);

CommonActionParams 参数规范

字段 类型 说明
MapData MapData 当前地图数据(必填)
PlayerData PlayerData 执行操作的玩家
UnitData UnitData 操作的单位
CityData CityData 操作的城市
GridData GridData 操作的格子
TargetUnitData UnitData 目标单位
TargetGridData GridData 目标格子
TargetPlayerData PlayerData 目标玩家
MainObjectType MainObjectType 操作主体类型(Unit/City/Grid/Player)

三、数据类 (Data) 开发规范

IdentifierBase 继承体系

IdentifierBase (uint Id, List<SkillBase> Skills)
  ├── PlayerData  ← 玩家数据
  ├── UnitData    ← 单位数据
  ├── CityData    ← 城市数据
  └── GridData    ← 格子数据

数据类必须遵循

  1. 标记 [MemoryPackable]partial
  2. 提供无参 [MemoryPackConstructor] 构造函数
  3. 提供拷贝构造函数 new XxxData(XxxData copyData) 用于 AI 深拷贝
  4. 提供 DeepCopy(XxxData copyData) 方法用于复用对象池的深拷贝
  5. 如有字典等非序列化字段,提供 [MemoryPackOnDeserialized] 标记的后处理方法
  6. 视觉标记字段用 [MemoryPackIgnore]

实体关系查询 (通过 MapData)

// Unit → Player
mapData.GetPlayerDataByUnitId(unitId, out PlayerData player);

// Unit → Grid
mapData.GetGridDataByUnitId(unitId, out GridData grid);

// Unit → City
mapData.GetCityDataByUnitId(unitId, out CityData city);

// Grid → Unit
mapData.GetUnitDataByGid(gridId, out UnitData unit);

// Grid → City (领土)
mapData.GetCityDataByTerritoryGid(gridId, out CityData city);

// Grid → Player (领土)
mapData.GetPlayerDataByTerritoryGridId(gridId, out PlayerData player);

// City → Player
mapData.GetPlayerDataByCityId(cityId, out PlayerData player);

// City → Grid (城中心)
mapData.GetGridDataByCityId(cityId, out GridData grid);

// Player → 首都
mapData.GetCapitalCityDataByPlayerId(playerId, out CityData capital);

// 快捷方式从UnitData直接访问
unitData.Player(mapData)   // → PlayerData
unitData.Grid(mapData)     // → GridData
unitData.City(mapData)     // → CityData

MapData 核心子结构

字段 类型 说明
GridMap GridMapData 所有格子(按坐标/ID索引
UnitMap UnitMapData 所有单位按ID索引
CityMap CityMapData 所有城市按ID索引
PlayerMap PlayerMapData 所有玩家
Net NetData 网络/回合信息
MapConfig MapConfig 地图配置

四、逻辑层 (Logic) 开发规范

入口访问

Main.UnitLogic    // UnitLogic 实例
Main.PlayerLogic  // PlayerLogic 实例
Main.CityLogic    // CityLogic 实例
Main.MapData      // 当前真实 MapData非 AI 模拟用)

视觉更新模式

// 格子视觉更新
gridData.Renderer(mapData)?.InstantUpdateGrid(true);

// 单位视觉更新
unitData.Renderer(mapData)?.InstantUpdateUnit(true);

// 城市信息视觉更新
cityData.SetCityRenderer(mapData);
cityData.CityInfoRenderer(mapData)?.InstantUpdateCityInfo();

// 播放特效(仅在玩家视野内)
gridData.Renderer(mapData)?.PlayVFXInSight(new GridVFXParams(GridVFXType.Fog));

// 判断是否在玩家视野内
gridData.InMainSight()
unitData.InMainSight()

AI 模拟 vs 真实 MapData

// 判断是否为真实地图非AI模拟副本
if (mapData == Main.MapData) { /* 只在真实地图执行 */ }

// 判断是否为当前显示的地图
if (mapData.IsCurrentShowMap()) { /* 执行视觉更新 */ }

// AI模拟使用的DeepCopy不拷贝 Players 和 Actions性能优化

随机数规范

// 必须使用 MapData 的确定性随机数(保证网络同步)
var random = mapData.Net.GetRandom(mapData);
var value = random.Next(0, 100);
// 禁止使用 System.Random 或 UnityEngine.Random会导致不同步

常用操作模板

// 视野更新
var gidList = mapData.GridMap.GetAroundGridIdList(sightRange, gridData);
Main.PlayerLogic.UpdateSight_LogicView(mapData, playerData, gidList);

// 城市经验更新
Main.CityLogic.GridGiveCityExp_LogicView(mapData, playerData, gridData, cityData, expAmount);

// 建筑等级刷新
Main.PlayerLogic.UpdateTerritoryAllBuildingLevel(mapData, playerData.Id);

// 城市连通性更新
Main.PlayerLogic.UpdateCityConnect(mapData, playerData);

// 伤害结算
Main.UnitLogic.DamageSettlement(mapData, attacker, target, damage, DamageType.ActiveAttack);

// 单位移动
Main.UnitLogic.MoveToLogic(mapData, unitData, gridData, MoveType.ActiveMove);

// 单位死亡
Main.UnitLogic.UnitUnnaturalDie(mapData, unitData);

// 单位类型变换
Main.UnitLogic.UnitTypeTransform(mapData, unitData, newUnitType);

五、关键枚举速查

UnitType单位类型

Warrior, Archer, Rider, Defender, Swordsman, Catapult, Knight, 
Ship, RammerShip, BomberShip, Giant(英雄), Minder, ...

TerrainType地形

Land, ShallowSea, DeepSea

TerrainFeature地貌

None, Mountain, Road

ResourceType资源/建筑)

None, Crop, Metal, Fruit, Animal, Fish,
Farm, Mine, LumberHut, Port, Bridge,
Windmill, Sawmill, Forge, Market, Academy, Military,
Temple, ForestTemple, WaterTemple, MountainTemple, KingTemple,
CityCenter, Wonder, NavalBase, ...

MoveType移动方式

ActiveMove, PassiveMove, AttackMove, SkillMove

DamageType伤害类型

ActiveAttack, CounterAttack, FollowAttack, Splash, True, KillSelf, DelayAttack, PushAttack

六、开发检查清单

新增技能时

  • SkillType 枚举已添加(只增不删)
  • 技能类文件在 AllSkill/ 目录下,类名为 XxxSkill,使用 partial class
  • 构造函数设置了 IsPermanentTurnsLimitScore
  • GetSkillType() 返回正确的枚举值
  • 如有自定义字段需要序列化,用 [MemoryPackInclude] 标记
  • 如有自定义字段需要拷贝,重写 GetSkillCopy()DeepCopy()
  • 表格配置已注册 skillPriority
  • 涉及视觉更新时判断了 mapData == Main.MapDataInMainSight()

新增行为时

  • 行为子类型枚举已添加
  • 实现了 Execute()CheckCan()CheckShow() 三个方法
  • Execute() 开头调用 CheckCan() 做鲁棒性保护
  • 已在 ActionLogicFactory 注册
  • 金钱消耗使用 playerData.SpendCoin(),金钱增加使用 playerData.AddCoin()
  • 建设/改造后调用 UpdateTerritoryAllBuildingLevelUpdateCityConnect

修改数据类时

  • 新增字段兼容 MemoryPack 序列化
  • 拷贝构造函数和 DeepCopy() 已更新
  • OnAfterMemoryPackDeserialize() 已更新(如涉及字典)

七、常见问题

Q: 技能应该挂到哪个实体上? A: 看技能作用范围。影响单个单位→挂UnitData影响玩家全局→挂PlayerData影响城市→挂CityData影响特定格子→挂GridData。

Q: CheckCanCheckShow 有什么区别? A: CheckCan 用于实际执行前的完整校验(包括金钱);CheckShow 用于UI展示不检查金钱只检查条件是否可见

Q: 为什么要用 mapData.GetXxxByYyy() 而不是直接引用? A: 实体间关系是动态的(如单位可以被转化、城市可以被占领),必须通过 MapData 的关系表实时查询。

Q: AI 模拟时要注意什么? A: AI 模拟使用 MapData 的深拷贝副本。不要在非 Main.MapData 上执行视觉更新。随机数必须使用 mapData.Net.GetRandom(mapData) 保证确定性。