21 KiB
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)
核心设计原则
- 数据与逻辑分离:
RuntimeData命名空间放纯数据,逻辑通过Main.UnitLogic、Main.PlayerLogic、Main.CityLogic静态入口访问 - MapData 是中心枢纽:所有跨实体查询都通过
MapData中转(如mapData.GetPlayerDataByUnitId()、mapData.GetGridDataByUnitId()) - IdentifierBase 四大实体:
PlayerData、UnitData、CityData、GridData均继承IdentifierBase,拥有uint Id和List<SkillBase> Skills - 逻辑-视觉分离:逻辑操作不直接操控视觉,通过
Renderer(mapData)?.InstantUpdateXxx()间接更新 - 序列化兼容:数据类使用
[MemoryPackable],必须提供[MemoryPackConstructor],私有字段需[MemoryPackInclude],非序列化字段用[MemoryPackIgnore]
一、新增技能 (Skill)
步骤
Step 1:在 SkillType 枚举中注册
在 SkillBase.cs 的 SkillType 枚举末尾(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:注册行为子类型枚举(如需)
在对应枚举中新增,如 UnitActionType、GridMiscActionType、PlayerActionType。
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 ← 格子数据
数据类必须遵循
- 标记
[MemoryPackable]和partial - 提供无参
[MemoryPackConstructor]构造函数 - 提供拷贝构造函数
new XxxData(XxxData copyData)用于 AI 深拷贝 - 提供
DeepCopy(XxxData copyData)方法用于复用对象池的深拷贝 - 如有字典等非序列化字段,提供
[MemoryPackOnDeserialized]标记的后处理方法 - 视觉标记字段用
[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 - 构造函数设置了
IsPermanent、TurnsLimit、Score GetSkillType()返回正确的枚举值- 如有自定义字段需要序列化,用
[MemoryPackInclude]标记 - 如有自定义字段需要拷贝,重写
GetSkillCopy()和DeepCopy() - 表格配置已注册
skillPriority - 涉及视觉更新时判断了
mapData == Main.MapData或InMainSight()
新增行为时
- 行为子类型枚举已添加
- 实现了
Execute()、CheckCan()、CheckShow()三个方法 Execute()开头调用CheckCan()做鲁棒性保护- 已在
ActionLogicFactory注册 - 金钱消耗使用
playerData.SpendCoin(),金钱增加使用playerData.AddCoin() - 建设/改造后调用
UpdateTerritoryAllBuildingLevel和UpdateCityConnect
修改数据类时
- 新增字段兼容 MemoryPack 序列化
- 拷贝构造函数和
DeepCopy()已更新 OnAfterMemoryPackDeserialize()已更新(如涉及字典)
七、常见问题
Q: 技能应该挂到哪个实体上? A: 看技能作用范围。影响单个单位→挂UnitData;影响玩家全局→挂PlayerData;影响城市→挂CityData;影响特定格子→挂GridData。
Q: CheckCan 和 CheckShow 有什么区别?
A: CheckCan 用于实际执行前的完整校验(包括金钱);CheckShow 用于UI展示(不检查金钱,只检查条件是否可见)。
Q: 为什么要用 mapData.GetXxxByYyy() 而不是直接引用?
A: 实体间关系是动态的(如单位可以被转化、城市可以被占领),必须通过 MapData 的关系表实时查询。
Q: AI 模拟时要注意什么?
A: AI 模拟使用 MapData 的深拷贝副本。不要在非 Main.MapData 上执行视觉更新。随机数必须使用 mapData.Net.GetRandom(mapData) 保证确定性。