Skip to content

单位系统(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 组件)

重要属性 ​

属性类型说明
IsAlivebool是否存活
IsDeadbool是否死亡
IsValidbool是否有效
PlayerPlayer所属玩家
PositionScenePoint当前位置
FacingAngle当前朝向

事件 ​

事件说明
OnDeath单位死亡
OnRevived单位复活
OnPositionChangingStart开始移动
OnPositionChangingEnd停止移动

相关文档 ​