主题
单位系统(Unit System)
单位系统为游戏中的角色、NPC、生物等提供管理和同步机制。基于服务端权威的 Entity 架构,支持属性、状态、视野、移动等核心功能。
需要在现有单位上增加 Node / 组件能力时,阅读单位、表现、场景与 GameGraph 的桥接。其中说明 GetNode() 的双端语义、直接动画接口、多用途绑定及位置控制归属。
系统概述
| 核心类 | 说明 |
|---|---|
Unit | 单位实体,继承自 Entity |
GameDataUnit | 单位数据定义和工厂 |
Player | 玩家对象,管理单位所有权和视野 |
Team | 队伍系统,管理玩家分组和视野共享 |
创建单位
使用 GameDataUnit.CreateUnit() 安全创建,失败时返回 null 而非抛异常:
csharp
#if SERVER
var unitData = myUnitLink.Data;
if (unitData == null)
{
Game.Logger.LogError("数编Link无效: {LinkName}", myUnitLink.FriendlyName);
return;
}
var unit = unitData.CreateUnit(player, position, facing);
if (unit == null)
{
Game.Logger.LogError("单位创建失败: {Position}, {Player}", position, player.Id);
return;
}
#endif不推荐直接使用 new Unit() 构造函数,因为创建失败时会抛出异常导致逻辑中断。
动态数值应修改实例,而不是反向修改 GameDataUnit
GameDataUnit 负责单位的静态定义;按波次成长、临时倍率、出生后附加状态等局内变化,应在 CreateUnit() 之后修改具体实例。
csharp
#if SERVER
var unitData = myUnitLink.Data;
var unit = unitData?.CreateUnit(player, position, facing);
if (unit == null) return;
// 推荐:修改这只刚创建出来的实例
unit.SetProperty(PropertyUnit.Level, waveLevel);
unit.SetProperty(PropertyUnit.AttackDamage, scaledAttack);
unit.SetProperty(PropertyUnit.Armor, scaledArmor);
// 不推荐:为了当前这一波怪去修改共享定义
// unitData.Properties[GSC.GameDataUnitProperty.AttackDamage] = scaledAttack;
#endif如果 myUnitLink.Data 来自 ScopeData 或数据编辑器生成文件,运行时直接修改它还可能导致客户端和服务端看到不同版本的同一单位定义。
工厂模式与继承
GameDataUnit 同时是 Unit 的工厂。继承 Unit 时必须同时继承 GameDataUnit 并重载 CreateUnit,否则创建的仍是基类 Unit:
csharp
// 步骤 1:继承 Unit
public class Hero : Unit
{
public Hero(IGameLink<GameDataUnit> link, Player player, ScenePoint scenePoint, Angle facing)
: base(link, player, scenePoint, facing)
{
}
public int HeroLevel { get; set; }
public void LevelUp()
{
HeroLevel++;
}
}
// 步骤 2:继承 GameDataUnit 并重载 CreateUnit
public class GameDataHero : GameDataUnit
{
public int BaseLevel { get; set; } = 1;
public override Unit? CreateUnit(Player player, ScenePoint scenePoint, Angle facing,
IExecutionContext? creationContext = null, bool useDefaultAI = false)
{
try
{
var hero = new Hero(Link, player, scenePoint, facing)
{
CreationContext = creationContext,
HeroLevel = BaseLevel
};
ScopeScript.LastCreatedUnit = hero;
return hero;
}
catch (Exception e)
{
Game.Logger.LogError(e, "创建英雄失败: {Position}", scenePoint);
return null;
}
}
}
// 步骤 3:使用
#if SERVER
var heroData = myHeroLink.Data as GameDataHero;
var hero = heroData?.CreateUnit(player, position, facing) as Hero;
hero?.LevelUp();
#endif其他组件也遵循相同的工厂模式:
csharp
public partial class GameDataAbility
{
public virtual Ability CreateAbility(Unit owner, Item? item = null);
}
public abstract class GameDataItem
{
protected abstract Item CreateItem(Unit unit);
}
public class GameDataInventory
{
public virtual Inventory CreateInventory(Unit unit);
}基本属性访问
csharp
var health = unit.GetProperty<float>(PropertyUnit.Health);
var isAlive = unit.IsAlive;
var position = unit.Position;
var facing = unit.Facing;
var owner = unit.Player;
#if SERVER
unit.SetProperty(PropertyUnit.Health, 100f);
unit.SetProperty(PropertyUnit.Level, 5);
#endif单位状态管理
csharp
#if SERVER
unit.AddState(UnitState.Invulnerable);
unit.AddState(UnitState.Stunned);
unit.RemoveState(UnitState.Stunned);
#endif
if (unit.HasState(UnitState.Dead))
{
// 单位已死亡
}生命周期管理
csharp
#if SERVER
unit.Kill(DeathType.Normal);
if (unit.IsDead)
{
unit.Revive();
}
#endif
unit.Destroy();异步操作
在 WebAssembly 环境中,必须使用框架的异步 API:
csharp
#if SERVER
public async Task DelayedHeal(Unit unit, float healAmount)
{
await Game.Delay(TimeSpan.FromSeconds(2.0f));
if (unit.IsAlive)
{
unit.Heal(healAmount);
}
}
public async Task ApplyTimedBuff(Unit unit, UnitState buffState, TimeSpan duration)
{
unit.AddState(buffState);
await Game.Delay(duration);
if (unit.IsValid)
{
unit.RemoveState(buffState);
}
}
// 禁止使用 Task.Delay() — 在 Wasm 中可能异常或不同步
#endif可见性与视野系统
视野机制
Sight属性控制视野范围。精确视野和扇形视野也使用同一个Sight最终值,不维护第二套距离- 单位对所有者玩家始终可见
- 地形和其他单位可能阻挡视野
- 草丛对内部单位无影响,对外部单位视为视野阻挡
GameDataUnit.Vision是可选的单位默认视野能力配置,目前只在精确视野模式下影响观察者选择、圆形/扇形形状和客户端本地视野遮罩表现
csharp
var sightRange = unit.GetPropertyComplex(myUnitPropertySight);
var canSee = unit.CanBeSeen(otherUnit);
var isVisible = unit.IsVisibleTo(somePlayer);需要配置场景级视野模式、观察者策略或扇形视野时,详见 场景视野系统。
可见性规则
| 关系类型 | 可见性 |
|---|---|
Player(自己) | 完全可见 |
Ally | 完全可见 |
Enemy | 视野内可见 |
Neutral | 视野内可见 |
客户端同步机制
WasiCore 采用服务端权威同步——客户端只接收视野内的单位数据。
同步类型
csharp
#if SERVER
unit.SetSyncType(SyncType.Sight); // 仅视野内同步(默认)
unit.SetSyncType(SyncType.All); // 全局同步
unit.SetSyncType(SyncType.Ally); // 盟友同步
#endif| 同步类型 | 描述 | 使用场景 |
|---|---|---|
None | 不同步 | 服务端专用单位 |
Self | 仅所有者 | 个人信息 |
Ally | 盟友可见 | 团队单位 |
All | 全局可见 | 重要单位 |
Sight | 视野内同步 | 普通单位(默认) |
SelfOrSight | 所有者或视野内 | 特殊单位 |
AllyOrSight | 盟友或视野内 | 团队特殊单位 |
客户端获取 Unit
通过 ID 获取:
csharp
#if CLIENT
Unit? unit = Entity.GetById(entityId) as Unit;
#endif通过 Player 获取:
csharp
#if CLIENT
Player localPlayer = Player.LocalPlayer;
Unit? mainUnit = localPlayer.MainUnit;
List<Player> allPlayers = Player.AllPlayers;
#endif通过 Scene 查询:
csharp
#if CLIENT
Scene currentScene = Player.LocalPlayer.Scene;
IEnumerable<Unit> allUnits = currentScene.GetMorphs(entity => entity as Unit);
IEnumerable<Unit> aliveUnits = currentScene.GetMorphs(entity =>
entity is Unit unit && unit.IsAlive ? unit : null);
#endif空间查询:
csharp
#if CLIENT
Scene scene = Player.LocalPlayer.Scene;
ScenePoint center = new ScenePoint(1000, 1000, scene);
// 圆形范围
var unitsInRange = scene.SearchCircle(center, 500f, entity => entity as Unit);
// 矩形范围
var unitsInRect = scene.SearchRectangle(
center, new Angle(0), 200f, 100f, entity => entity is Unit unit && unit.IsAlive);
// 扇形范围
var unitsInCone = scene.SearchCone(
center, new Angle(45), 300f, new Angle(90), entity => entity is Unit);
#endif事件监听:
csharp
#if CLIENT
public class UnitWatcher : IGameClass
{
public static void OnRegisterGameClass()
{
Game.OnGameTriggerInitialization += () =>
{
Trigger<EventUnitCreated>.Subscribe(e =>
{
Unit newUnit = e.Unit;
// 处理新单位
});
Player.LocalPlayer.OnMainUnitChanged += OnMainUnitChanged;
};
}
}
#endif客户端操作权限
可以进行的操作:
- 读取属性(血量、位置、状态等,只读)
- 对自己的单位发送指令(通过指令系统)
- 创建视觉效果(ActorHighlight、ActorParticle 等)
无法进行的操作:
- 直接修改 Unit 属性
- 创建或销毁 Unit
- 对其他玩家的 Unit 下指令
- 访问不可见 Unit 的详细信息
队伍视野共享
同一队伍的玩家默认共享视野:
csharp
#if SERVER
// 自定义视野共享
player1.SetShareSightTo(player2, true); // 开启共享
player1.SetShareSightTo(player2, false); // 关闭共享
// 检查视野共享状态
bool isSharing = player1.IsShareSightTo(player2);
#endif视野共享规则:
- 队伍成员自动共享视野
- 可与非队伍成员自定义共享
- 共享可以是单向的
- 状态变化实时生效
移动系统
单位移动有两种方式:通过指令系统(受队列管理)和直接方法(绕过队列)。
关于指令系统移动(Move、VectorMove 命令),参见 指令系统。
直接移动方法
直接调用的移动方法不受指令队列限制,可在技能释放期间使用:
csharp
#if SERVER
// 瞬移到指定位置
unit.MoveTo(new ScenePoint(500, 300, scene));
// 摇杆移动(持续方向移动)
unit.StartJoystickMove(new Angle(45));
unit.StopJoystickMove();
// 通过 Walkable 组件寻路
var walkable = unit.GetComponent<Walkable>();
walkable?.PathTo(target, maxDistance: 100f);
#endif移动状态与限制
csharp
#if SERVER
bool isMoving = unit.HasState(UnitState.IsMoving);
if (unit.HasState(UnitState.Rooted))
{
// 被束缚,无法移动(可攻击和施法)
}
if (unit.HasState(UnitState.Stunned))
{
// 被眩晕,无法执行任何操作
}
#endif| 状态 | 效果 |
|---|---|
Rooted | 不能移动,可以攻击和施法 |
Stunned | 不能执行任何操作 |
Slowed | 移动速度降低 |
位移技能示例
csharp
#if SERVER
// 闪现技能
public void CastBlink(Unit caster, ScenePoint targetPosition)
{
var maxRange = 1200f;
if (caster.Position.DistanceTo(targetPosition) > maxRange)
{
targetPosition = caster.Position.MoveTowards(targetPosition, maxRange);
}
caster.MoveTo(targetPosition);
}
// 冲锋技能
public async Task CastCharge(Unit caster, Unit target)
{
var chargeSpeed = 800f;
var originalSpeed = caster.GetProperty<float>(PropertyUnit.MoveSpeed);
caster.SetProperty(PropertyUnit.MoveSpeed, chargeSpeed);
var walkable = unit.GetComponent<Walkable>();
walkable?.PathTo(target, maxDistance: 100f);
while (caster.Position.DistanceTo(target.Position) > 150f && caster.IsValid)
{
await Game.DelayFrames(1);
}
caster.SetProperty(PropertyUnit.MoveSpeed, originalSpeed);
}
#endif移动同步
无论使用哪种移动方式,服务端位置变化自动同步到客户端。客户端可监听位置事件:
csharp
#if CLIENT
unit.OnPositionChangingStart += (entity) =>
{
// 单位开始移动
};
unit.OnPositionChangingEnd += (entity) =>
{
// 单位停止移动
};
#endif高级配置
单位头顶状态栏(血条)
GameDataUnit.StatusBarSetting 控制单位头顶血条/状态栏。单位默认没有头顶血条;需要显式配置 DefaultStatusBar 或 OverrideByRelationShip。
Hero 系列状态栏左侧有等级槽。这个槽位读取单位等级;如果单位没有 Level,也没有 Leveling/UnitLeveling 数据,左侧会显示一块黑色区域。
- 不想显示等级:使用 Normal 系列状态栏。
PlayerUnitRelationShip.MainUnit没有MainNormal*预设,主控单位也可以指向AllyNormalNone/AllyNormalMana/AllyNormalExp等 Normal 变体。 - 想显示等级:使用 Hero 系列状态栏,并同时配置
Leveling = GSC.GameDataUnitLeveling.HeroLevelingSample和Level = 1。
csharp
StatusBarSetting = new()
{
DefaultStatusBar = GSC.GameDataStatusBar.EnemyNormalNone,
OverrideByRelationShip = new()
{
{ PlayerUnitRelationShip.Alliance, GSC.GameDataStatusBar.AllyNormalNone },
{ PlayerUnitRelationShip.Enemy, GSC.GameDataStatusBar.EnemyNormalNone },
{ PlayerUnitRelationShip.Neutral, GSC.GameDataStatusBar.NeutralNormalNone },
},
},GSC 预置的 Normal 系列无等级槽:AllyNormalNone / AllyNormalMana / AllyNormalExp、EnemyNormalNone / EnemyNormalMana / EnemyNormalExp、NeutralNormalNone / NeutralNormalMana / NeutralNormalExp。
GSC 预置的 Hero 系列有等级槽:MainHeroNone / MainHeroMana / MainHeroExp / MainHeroEnergy / MainHeroAnger、AllyHeroNone / AllyHeroMana / AllyHeroExp / AllyHeroEnergy / AllyHeroAnger、EnemyHeroNone / EnemyHeroMana / EnemyHeroExp / EnemyHeroEnergy / EnemyHeroAnger、NeutralHeroNone / NeutralHeroMana / NeutralHeroExp / NeutralHeroEnergy / NeutralHeroAnger。
GameDataUnit 数据配置
下面是框架内置 GameDataUnit 的声明节选,用于说明字段结构,不是让项目重复定义该分类。项目自定义可视化单位类型时,应在触发编辑器创建 Class 并“设置为数编类型”;纯代码项目才直接声明 C# 特性。详见 GameData 数据驱动系统。
csharp
[GameDataCategory]
public partial class GameDataUnit : IGameDataActorScopeOwner
{
public float AttackableRadius { get; set; }
public float CollisionRadius { get; set; }
public List<UnitFilter> Filter { get; set; } = [];
public List<UnitState>? State { get; set; }
public DynamicCollisionMask DynamicCollisionMask { get; set; } =
DynamicCollisionMask.Unit | DynamicCollisionMask.Building;
}属性系统集成
csharp
var propertyComplex = unit.GetComponent<UnitPropertyComplex>();
#if SERVER
propertyComplex.SetFixed(mySightProperty, PropertySubType.Base, 800f);
propertyComplex.SetFixed(myMoveSpeedProperty, PropertySubType.Base, 350f);
propertyComplex.AddFixed(mySightProperty, PropertySubType.Bonus, 200f);
#endif
var finalSight = propertyComplex.GetFinalValue(mySightProperty);API 参考
创建与销毁
| 方法 | 说明 |
|---|---|
GameDataUnit.CreateUnit(player, point, facing) | 安全创建(推荐) |
Unit.Kill(DeathType) | 杀死单位(服务端) |
Unit.Revive() | 复活单位(服务端) |
Unit.Destroy() | 普通主动销毁单位,执行完整的独立清理 |
Unit.Destroy(bool isHostDispose) | 旧兼容重载;不要在新代码中传入 true |
DestroyFromHost() 是宿主级销毁入口。它是 protected internal,框架内部以及确认宿主正在级联销毁的自定义 Unit 派生类可以调用。整个 Scene 卸载时,底层场景会统一销毁单位的非托管部分,因此框架对场景内单位只做托管清理,避免逐个单位重复移除场景或释放底层对象。调用它会将 hostDisposedValue 设置为 true;自定义 Unit 派生类可以据此在销毁重写中判断宿主是否已经接管底层清理。游戏逻辑主动销毁单个单位时必须调用 unit.Destroy();不要把 unit.Destroy(true) 当作强制删除,也不要用它替代 unit.Kill(...) 的死亡流程。
属性管理
| 方法 | 说明 |
|---|---|
GetProperty<T>(PropertyUnit) | 获取属性值 |
SetProperty<T>(PropertyUnit, T) | 设置属性值(服务端) |
AddProperty(PropertyUnit, Fixed) | 增加属性值(服务端) |
状态管理
| 方法 | 说明 |
|---|---|
AddState(UnitState) | 添加状态(服务端) |
RemoveState(UnitState) | 移除状态(服务端) |
HasState(UnitState) | 检查状态 |
可见性
| 方法 | 说明 |
|---|---|
IsVisibleTo(Player) | 对玩家是否可见(服务端) |
CanBeSeen(Entity) | 可被实体看到 |
IsValidTargetTo(Entity, bool) | 是否为有效目标 |
移动控制
| 方法 | 说明 |
|---|---|
MoveTo(ScenePoint) | 瞬移(服务端) |
StartJoystickMove(Angle) | 开始摇杆移动(服务端) |
StopJoystickMove() | 停止摇杆移动(服务端) |
Stop() | 停止所有移动(服务端) |
SetFacing(float, TimeSpan) | 设置朝向(服务端) |
Walkable.PathTo(ITarget, ...) | 寻路移动(需获取 Walkable 组件) |
重要属性
| 属性 | 类型 | 说明 |
|---|---|---|
IsAlive | bool | 是否存活 |
IsDead | bool | 是否死亡 |
IsValid | bool | 是否有效 |
Player | Player | 所属玩家 |
Position | ScenePoint | 当前位置 |
Facing | Angle | 当前朝向 |
事件
| 事件 | 说明 |
|---|---|
OnDeath | 单位死亡 |
OnRevived | 单位复活 |
OnPositionChangingStart | 开始移动 |
OnPositionChangingEnd | 停止移动 |
相关文档
- 指令系统 — 通过指令系统控制单位移动和攻击
- 3D 单位移动控制器 — 3D 场景、物理角色、跳跃、空中跳和 locomotion 状态机
- 属性系统 — 单位属性管理
- AI 系统 — NPC 单位的 AI 行为
- 触发器/事件系统 — 单位事件处理