主题
WasiCore 框架开发指南
按目标组织开发
用户经验用于决定解释方式,玩法需求决定实现方案。对零基础用户先完成可玩的最小目标;对地图编辑器或 Unity/Urho 用户,可用熟悉的概念帮助理解。SCE 数据、事件、单位、技能和物品可以构成完整玩法,也可以按功能结合 GameGraph Node 与组件。使用 AI 或下钻 API 不要求放弃原有图形化数据、触发器或合适的项目结构。
需要选型、讲解对象关系或跨层扩展时,读 wasicore-game-architecture。该 skill 指向经验迁移指南和桥接参考;不要因单独出现“血量”“3D”或“联机”就替整个项目固定技术路线。
开发入口与运行验证
用户只有安装目录、项目尚未创建,或新项目还没有完成第一次连服时,先读首次上手 Skill,再进入具体开发流程。
按开发入口规则选择宿主:用户已打开目标项目的可视化编辑器时优先复用,除非用户主动要求隔离;没有已打开的 GUI 或用户要求隔离时,AI 创建、编辑数据与触发器、构建、单客户端连服调试和发布优先 Headless。UI Authoring、地形交互和编辑器资源预览使用 GUI;本轮明确只验证客户端局部表现时可走纯客户端预览。Headless 当前只编排一个客户端,多客户端测试使用完整编辑器的多人调试流程。
先发现安装版本的 capability 和工具 schema。标准构建/调试工具负责生成、双端编译和部署;复用宿主与账号,跟踪原构建/操作 ID,避免超时后重复启动。GUI 和 Headless 不要同时写同一项目。运行验证按 sce-preview-verify 收集当前局截图、Runtime MCP 和双端日志;Headless 可启动有窗口的客户端,仍需检查实际游戏表现。项目内 Skill 镜像的指南链接若不可用,按项目根目录下的 docs/sdk/guides/Editor2PublishAndAiWorkflow.md 和 docs/sdk/guides/HeadlessEditor.md 阅读。
无头开发开始前,已知实际编辑器安装路径时,先用该安装根的 sce.cmd prepare <项目目录> 联网准备当前渠道/API、遵守项目版本约束的 SDK、文档和 AI 上下文;核对成功后重新读取生成的项目规则和 Skill。路径未知时先定位,无法确定则说明尚未准备,可用现有 SDK 继续,不猜路径或宣称已更新。刚成功 init 或本轮已准备过可跳过;连续迭代复用宿主,无需逐次刷新。已有项目宿主占用时先保存、结束活动并正常关闭,再准备重开,不强关用户工作。serve 不刷新这些内容,--offline 也不能证明线上最新。完整步骤见开发前准备最新 SDK 和文档。
GameGraph 与 Urho API
GameGraph 主要将星火所用 Urho 引擎的常用游戏开发 API 导出并封装为 C# 接口,同时补充星火特有能力,以及与 GameCore 单位、表现和场景系统的桥接。AI 可以利用已有的 Urho 知识寻找实现思路,再通过当前 SDK 的 api/client / api/server 声明确认类型、签名和可用端;星火扩展与行为差异以对应专题文档为准。这适用于直接 Node 开发,也适用于现有 Unit 项目下钻,具体能力按客户端或服务端声明使用。
统一说明见 GameGraph 总览的“与 Urho3D 的兼容心智模型”。若项目内 skill 副本的相对链接不可用,在 docs/sdk/systems/GameGraphOverview.md 查阅。
编译配置(最重要)
WasiCore 使用条件编译区分客户端和服务端代码。必须指定正确的配置:
bash
dotnet build *.sln -c Server-Debug # 编译服务端代码
dotnet build *.sln -c Client-Debug # 编译客户端代码(UI、Canvas等)不指定配置或用 -c Debug 会导致 3000+ 编译错误(#if CLIENT / #if SERVER 包裹的代码不会被编译)。
遇到编译错误时,首先检查编译配置是否正确。
引用依赖库项目
接入或排查依赖库时,先读依赖库项目指南。项目内镜像链接不可用时,查 docs/sdk/guides/DependencyLibraries.md。
- 带代码的依赖库同时需要地图根目录
libs.json的依赖声明与src/GameEntry.csproj的ProjectReference或Reference。先检查现有配置并合并,保留其他依赖;两者不会自动替代。纯数据/资源库没有 C# 工程时,不要伪造 csproj 或 DLL。 - 本地源码库可放在编译机可访问的任意位置。csproj 的
Include/HintPath只决定编译来源,实际运行时不会使用该路径查找库。源码引用使用匹配的双端配置;DLL 引用按Server-*/Client-*选择对应端产物,并覆盖需要的Client-Resource配置。避免同一库同时引用工程与旧 DLL。 - 本地 GUI/客户端联调时,在地图根目录
local_libs.json(复数)将已声明的库 ID 映射到库项目根目录;这不代替libs.json。Headless 托管数据/触发器上下文当前还需显式--lib ID=路径,按指南中的相对路径拼接规则配置,不能假设自动读取本地覆盖文件。核对三处实际解析到同一份库。 - 运行时按程序集名查依赖 ID,再从库包加载同名 DLL。库 ID、AssemblyName 与输出 DLL 基名须大小写不敏感地一致;普通地图保持 GameEntry。检查双端库包,不要把同名 DLL 放进地图 AppBundle 来替代库包,也不要无必要改名既有官方工程。
- 引用编译成功不等于库资源准备或发布完成。先更新库产物、数据与资源,再构建并在引用地图中验证;正式发布需要目标环境可下载的库包,不能依靠个人绝对路径或本地覆盖。库商店发布仍走 GUI 元数据/审核流程。
战斗 AI 选敌排序
配置战斗 AI 选敌时,区分树级 ScanSorts 标签优先级与节点级 TargetSorts:节点未配置时使用树级默认,显式空列表表示不排序,自定义比较函数则完全覆盖默认规则;需要标签优先级时由函数读取 thinkTree.Cache.ScanSorts 自行组合。保持合法旧目标时不会重新排序。GameSparkCore 默认公式显式采用标签优先、同级最近的策略,详见 AI 系统;选敌行为异常时按 behavior-troubleshoot 排查。
响应与附加效果目标
配置反伤、受击自疗、吸血或死亡复活,以及排查响应目标错误时,读 response-system。原事件的主动方/被动方与响应附加效果的 Caster/Target 分别判断;Location=Defender 的附加效果默认指向主动方,单位直接挂载的自疗/自我复活需显式选自己。Buff 响应的施法者可能是 Buff 施加者。
项目自定义 AI 说明
项目根目录的 AGENTS.md、CLAUDE.md 和 .cursor/rules/project-context.mdc 是自动生成文件,编辑器打开项目或运行 tools/setup-ai-context.ps1 时会刷新。不要把用户自定义说明直接写进这些生成文件。
如果项目需要补充团队约定、玩法背景、目录说明或 AI 助手长期应遵守的项目规则,在项目根目录创建 AGENTS.project.md。生成器会在该文件存在且非空时,把内容合入上述三个 AI 上下文入口的 Project Custom Instructions / 项目自定义说明 段;AGENTS.project.md 本身不会被生成器创建、覆盖或删除。
AGENTS.project.md 默认不是生成物,也不应默认加入 .gitignore,团队可以选择提交它作为项目约定。若提交该文件,不要写入 token、个人账号、私有本机路径或其他敏感信息。个人本机临时偏好应另找本地忽略文件承载,不要混入团队共享说明。
条件编译代码结构
csharp
#if CLIENT
using GameUI.Control.Primitive;
using GameUI.Graphics;
namespace YourGame
{
public class GameRenderer { }
}
#endif
#if SERVER
using GameCore.ServerSystem;
namespace YourGame
{
public class GameServer { }
}
#endifGameData 公式字段赋值位置
手写 C# GameData 时,先按字段可见性判断,再按公式偏向判断:
- 字段只存在于服务端 API:只在
#if SERVER中赋值。 - 字段只存在于客户端 API:只在
#if CLIENT中赋值。 [DataEditor(FunctionBias = FunctionBias.Server)]是服务端公式偏向,不是字段服务端专属;默认只在#if SERVER中赋值,尤其是GameDataEffectXXX效果节点。[DataEditor(FunctionBias = FunctionBias.Client)]默认只在#if CLIENT中赋值。- 客户端 UI / 预览确实要读取同一个公式字段,且公式只用常量或双端共有 API 时,可以共享赋值,例如
Range = static (_) => 500。 - 不要把所有 GameData 都放进
#if SERVER:表现、Actor、UI、单位显示数据仍然需要客户端定义。
详细例子见 docs/best-practices/GameDataFormulaPlacement.md。
DualEndEvent 事件数据必须用属性
双端事件内部走 System.Text.Json 序列化,并使用框架默认的 MessageSerializerOptions.Default。该配置没有开启 IncludeFields,所以事件 payload 必须写成公开 get; set; 属性。不要用字段承载需要跨端传递的数据;字段即使能编译,也不会按框架默认配置被 DualEndEvent 序列化/反序列化,对端通常会拿到默认值。
csharp
// 错误:Score 是字段,不要用它传双端事件数据
public class ScoreChangedEvent : IDualEndEvent<ScoreChangedEvent>
{
public Player? Sender { get; set; }
public int Score;
}
// 正确:所有要传输的数据都是属性
public class ScoreChangedEvent : IDualEndEvent<ScoreChangedEvent>
{
public Player? Sender { get; set; }
public int Score { get; set; }
}Sender 也必须是 Player? Sender { get; set; } 属性,客户端发到服务端时由框架自动填充。完整说明见 docs/systems/DualEndEventSystem.md。
UI 控件生命周期
关闭后不再复用的 UI 控件必须调用 Destroy()。RemoveFromParent() / RemoveFromVisualTree() 只把控件从 UI 树摘下,ClearChildren() 只移除直接子控件;这些 API 不释放控件、子控件树、事件或 native control。
- 临时隐藏、换父节点、对象池复用:可以用
RemoveFromParent()/ClearChildren()。 - 浮字、弹窗、临时面板、战斗 HUD、过期特效画布、
Canvas/CanvasAnimated:关闭后不复用时用Destroy()。 - 手动订阅
OnRender/OnAnimatedRender或全局输入事件的业务对象,建议先取消订阅再销毁控件。
Destroy() 与宿主级销毁
普通游戏逻辑销毁对象时调用 Destroy()。它表示对象独立结束生命周期,会执行对象自身的完整清理,并在适用时从所属场景、宿主集合和运行时索引中移除。
DestroyFromHost() 是宿主级生命周期入口,不是普通游戏逻辑的替代写法。它是 protected internal:框架内部可以调用;确认宿主正在级联销毁的 DisposableObject / Component / Unit 自定义派生类也可以调用。宿主(例如整个 Scene)正在卸载时,底层场景会统一销毁单位和实体的非托管部分;框架只需要清理托管状态,并跳过逐个对象再次移除场景或释放底层句柄。它会在进入销毁流程前设置受保护的 hostDisposedValue = true。
如果你在 DisposableObject、Component 或 Unit 上编写自定义派生类,可以在 DisposeManaged / DisposeUnmanaged 等销毁重写中读取 hostDisposedValue:当它为 true 时,说明宿主正在负责级联销毁,必要时应跳过宿主已经完成的清理,或执行只适用于宿主销毁的特殊逻辑。普通主动销毁走 Destroy() 时该标记为 false。
单位尤其要遵守这条边界:
- 主动移除一个仍在运行的单位:
unit.Destroy()。 - 整个场景卸载时:由框架内部对场景中的单位使用
DestroyFromHost()。 - 不要把
Destroy(true)当作“强制删除单位”;true是旧的isHostDispose生命周期标记,错误地对活动单位使用可能留下已释放但仍被场景集合持有的对象。 Unit.Kill(...)是死亡流程,涉及死亡事件、掉落和可复活语义;它与Destroy()不是一回事。
运行时日志(排查“表现不对”时必看)
通过编辑器启动地图后,运行时日志默认写到编辑器安装目录,而不是地图工程根目录:
- 日志根目录:
<EditorRoot>/logs(包含client、server、lua、CSharp、Network等子目录,以及根目录下的零散日志文件) - 客户端运行时日志:
<EditorRoot>/logs/client - 服务端运行时日志:
<EditorRoot>/logs/server - 编辑器 Lua 日志:
<EditorRoot>/logs/lua
常见安装示例:E:/WasmSCE/logs,其中客户端和服务端日志分别在 client、server 子目录。
常见误区:
- 地图根目录里的
Client-Debug.log/Server-Debug.log往往只是dotnet build输出,不是实际跑图日志。 - 真正的运行时日志文件名通常类似
wasm-default-*.log-*.log(client)和wasm-game-server-*.log(server)。 - 如果需求只说“编辑器日志”,先列出并按时间排序
<EditorRoot>/logs,再进入client、server、lua或其它子目录。
遇到“编译通过,但首波怪不动 / UI 不显示 / 事件没触发”这类问题时,先看 Game.Logger 输出是否进入了正确分支,再去上述运行时日志里按关键字定位。
WASI 无线程池不只影响 Task.Run():TaskCompletionSource 本身可以使用,但 new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously) 会把 continuation 投递到 ThreadPool,在 .NET WASI 中触发 PlatformNotSupportedException。需要把回调转成 await 时,使用默认构造函数 new TaskCompletionSource<T>()。
自定义托管程序集
项目需要拆分业务代码或引入项目自有托管 DLL 时,优先把它们作为 src/GameEntry.csproj 的普通 .NET 依赖管理,例如 ProjectReference、PackageReference,或带明确来源的 Reference。然后使用编辑器的正常编译、调试和发布流程部署产物。
服务端项目自有 DLL 放在 AppBundle/managed/,客户端项目自有 DLL 放在 ui/AppBundle/managed/,也就是和对应端的 GameEntry.dll 同级。自定义 DLL 名称应使用项目自己的唯一名称,避开 SDK 自带程序集名称和依赖库 ID;若 DLL 名称对应某个依赖库 ID,该 DLL 由对应依赖库提供,不由主项目提供。SDK 与运行时组件由当前编辑器/SDK 版本统一提供。
若用户说自定义 DLL 已经放到 managed 目录但没有生效,先提醒检查 DLL 文件名是否与 SDK 自带程序集或项目依赖库 ID 重名。若必须在 debug_start_no_compile 这类跳过编译的本地循环中运行额外业务 DLL,先正常构建对应 Client/Server 配置,再把项目自己的输出程序集随 GameEntry.dll 放入对应端的 managed 目录。完整说明见 docs/guides/CustomManagedAssemblies.md。
纯客户端调试
编辑器的纯客户端调试会以 -client_only 启动单独客户端进程,不启动服务端。代码中用 Game.IsClientOnly 判断:
csharp
if (Game.IsClientOnly)
{
// UI 假数据、GameGraph 原型、模型场景渲染测试、本地调试工具等
}纯客户端模式下 Entity / Unit / CloudData / 服务端消息不可用,但纯表现层 Actor(如 ActorSound、ActorModel、ActorParticle)、GameGraph 和 UI 可用于本地预览。需要加载数编场景资产时,在 #if CLIENT 且 Game.IsClientOnly 分支中使用 Scene.GetOrCreate(ScopeData.GameDataScene.xxx).LoadClientOnly();如果只需要当前 GameMode 的默认场景,使用 Scene.LoadDefaultClientOnly()。项目也可以在 GameDataGameMode 上设置 AutoLoadDefaultSceneInClientOnly = true,让纯客户端启动后自动加载 DefaultScene,该配置默认关闭。普通客户端/服务端流程仍由服务端切场景,不要用客户端本地切场景替代。主项目 user_files 在客户端编辑器调试阶段可写,包括纯客户端调试;依赖库 deps/.../user_files 和 AppBundle 根目录仍只读。完整说明见 docs/guides/ClientOnlyDebug.md 与 docs/guides/ProjectFileAccess.md;需要 AI 直接启动客户端测试时使用 docs/ai/skills/client-only-debug/SKILL.md。
SCE 场景与 GameGraph 速记
如果用户只说“2D 游戏”或“3D 游戏”,先读 docs/ai/skills/wasicore-game-architecture/SKILL.md。先判断项目走 SCE Unit/技能系统、直接 GameGraph SceneGraph,还是 Canvas/GameUI 绘制路线,再选择具体 skill。
普通联机流程里,每个客户端同一时刻最多绑定一个当前 SCE Scene;这个场景由框架加载、切换和卸载,不需要脚本手动维护其生命周期。SCE Scene 内置一个 GameGraph SceneGraph 包装,可通过 using GameGraph.NodeSystem; scene.GetSceneGraph() 获取;同步挂载根用 scene.GetSyncRoot(),向当前 SCE 场景加入同步节点用 scene.AddSyncNode(node, NodeReplication.Replicated)。
GameDataScene.SpaceModel 是寻路/高度/碰撞语义,不是渲染维度。SceneSpaceModel.Surface2D 表示 2.5D 单层地表寻路:导航投影在 XY 平面,但 SCE 场景仍可有起伏地形、高台、坡面和 Z 高度;不要把它理解成 Canvas/纯 2D 场景或无高度场景。只有同一 XY 位置需要桥、隧道、上下楼层等多层可走面时,才使用 SceneSpaceModel.Volumetric3D 和对应 3D 导航产物。
脚本 new SceneGraph(SceneGraphReplication.ReplicatedUnpublished) 创建的是独立发布图,不是 SCE 场景。服务端可以按玩家 Publish(player) / Unpublish(player);同一个客户端可以在当前 SCE 场景之外同时订阅多个这种发布图。客户端 SceneGraph.OnRegistered / GetRegisteredSceneGraphs() 面向这些额外发布图,不是获取当前 SCE 场景的入口。当前 SCE 场景即使正在正常同步,也不会出现在 GetRegisteredSceneGraphs() 结果里;排查 SCE 场景内 replicated node 时,从当前 Scene / Unit.Scene 调 GetSceneGraph() 后轮询 GetReplicatedNodes(...),或在拿到该 SceneGraph 后订阅它的 OnNodeReplicated。
发布图注册不等于自动渲染。独立 SceneGraph 可以只作为同步数据源使用,例如客户端读取节点状态后绘制到 Canvas/UI,而不创建 Camera 或接管 Viewport。若要直接显示为 3D 画面,客户端需要配置 Octree、local 表现组件、CameraComponent 和 Viewport;底层入口是 Renderer.SetupMainViewport(...),SceneGraphMainViewportController 只是可选 helper。
GameGraph 3D 场景必须在初始化时为每个实际参与渲染的 Viewport 主动设置 RenderPath,避免不同调试配置下默认渲染管线不一致。不要假设附加 Viewport 或 RTT Viewport 会自动沿用主视口的 RenderPath:
csharp
Renderer.SetupMainViewport(sceneGraph, camera);
Renderer.GetMainViewport()?.SetRenderPath(
"EngineRes/RenderPaths/CEMapSSAO.xml"u8);常用内置 RenderPath:
| 路径 | 用途 |
|---|---|
EngineRes/RenderPaths/EditorMapSSAOBloomHDR.xml | 编辑器主视口同款;当前默认不呈现场景雾效,适合明确需要忽略雾的技术视图 |
EngineRes/RenderPaths/CEMapSSAO.xml | 游戏运行时常用 Forward 光照 + SSAO;会呈现场景雾效,适合对齐独立客户端和对战平台表现 |
EngineRes/RenderPaths/CEMapDeferred.xml | 游戏运行时 Deferred 光照 + SSAO |
EngineRes/RenderPaths/EditorMapDeferred.xml | 编辑器 Deferred 变体 + SSAO |
EngineRes/RenderPaths/CEMapUnderUISSAO.xml | UI 之下的游戏渲染变体,Forward 光照 + SSAO |
EngineRes/RenderPaths/GameOverlay.xml | 直接附加 viewport 的透明叠加,只清理深度、不清理颜色、不执行全屏后处理 |
附加 Viewport 和 RenderToTexture Viewport 都必须独立 SetRenderPath(...)。当前默认资源配置下,编辑器内嵌调试常用的 EditorMapSSAOBloomHDR.xml 不呈现场景雾效,而独立客户端常用的 CEMapSSAO.xml 会呈现雾效;漏设 RenderPath 会造成“编辑器里没有雾,客户端里有雾”的错觉。小地图或远距离俯视镜头可能在客户端整幅被雾遮住。普通游戏镜头要对齐客户端时显式用 CEMapSSAO.xml;明确需要忽略雾的技术视图或小地图显式用 EditorMapSSAOBloomHDR.xml;透明叠加用 GameOverlay.xml。推荐 init 时只设一次;运行时频繁切换可能在 D3D11 debug layer 下产生 framebuffer validation 警告。做画中画或多摄像机画面时,不要硬编码 viewport 编号,优先用 Renderer.GetNextFreeViewportIndex();排查占用时用 Renderer.GetOccupiedViewportIndices()。Viewport.SetRect(...) 使用当前 render target 的像素坐标,编辑器内嵌调试时坐标以编辑器管理的游戏画布 RenderTarget 为基准。
生成 SceneGraph stencil 示例时,用材质状态和 RenderOrder 表达遮罩顺序:写入材质先渲染,通常设置 CompareMode.Always、PassOp.Ref、StencilWriteMask = 0xFF;读取材质后渲染,按需求使用 CompareMode.Equal 或 CompareMode.NotEqual,保持相同 StencilRef,设置 StencilReadMask = 0xFF、StencilWriteMask = 0。预期现象是读取几何只在写入区域内或区域外显示;若只是普通叠色,先查 render order、ref/mask 和 RenderPath。完整写法见 docs/systems/MaterialSystem.md#模板测试stencil。
材质选择与不可见物体排查优先读 docs/ai/skills/gamegraph-materials/SKILL.md。当前对外简单纯色材质用 MaterialFlatLambert,不要生成 MaterialUnlit 或 NoTextureUnlit* 用法;Mesh 粒子 technique 优先用 Editor/Techniques/Lambert/ParticleLambert.xml;水体用 MaterialWater / EngineRes/Techniques/SingleLayerWater.xml。GameGraph 3D 物体看不到时,先通过 Runtime MCP 调 material.probe 或 scenegraph.materials,检查实际 technique path、base/alpha/depth/singlelayerwater pass、alpha、render order、stencil 和 cull/fill 状态,再改代码或材质。
RenderToTexture 流程:Texture2D.CreateRenderTarget(...) 创建纹理,取 texture.RenderSurface,创建独立 Viewport,设置 surface.NumViewports、surface.SetViewport(...) 和 surface.UpdateMode。RTT 结果可以挂到材质贴图槽,也可以进入 GameUI:普通控件用 RuntimeTexture.Register("Name", texture) 或 .Image(texture, "Name") 注册为 RT: 运行时图片;Canvas 可直接 canvas.DrawTexture(texture, x, y, w, h)。透明 UI 合成使用 RenderTargetFormat.Rgba8(HDR 可用 Rgba16F / Rgba32F)以保留 alpha;旧的 highDynamicRange: false 重载使用 RGB,不提供可依赖的 alpha。完整代码示例见 docs/systems/GameGraphOverview.md#客户端-viewport-与-renderpath。
编辑器内嵌调试模式下,主 GameGraph 视口用 Renderer.SetupMainViewport(...),不要用 CreateViewport + SetViewport(0, ...) 替代。若直接调用 Renderer.SetViewport(...),SDK 会发出一次性 warning:内嵌调试的 viewport/render-target 配置、默认 RenderPath 和实机或独立客户端不同,手动 viewport 编号、SetRect 坐标及雾效表现都不一定代表真实运行结果。测试画中画、多 viewport、远距离小地图或 RenderToTexture 时,必须确认每个 Viewport 已显式设置 RenderPath,并在编辑器启动参数中加 -no_debug_game_in_editor,或使用纯客户端调试模式完成最终截图验收。独立进程验证的重要原因之一,就是让画面走客户端实际 RenderPath,暴露内嵌编辑器默认无雾而客户端有雾的问题。完整说明见 docs/systems/GameGraphOverview.md#客户端-viewport-与-renderpath。
Replicated Node 的 transform 在客户端默认会用 Urho SmoothedTransform 平滑到最新网络目标。平滑参数在 SceneGraph 上配置:TransformSmoothingConstant、TransformSnapThreshold 或 ConfigureTransformSmoothing(...)。这些值不随网络同步;调整客户端视觉表现时应在客户端拿到对应 SceneGraph 后设置。
Replicated Node 支持轻量 SyncVar:服务端用 SetSyncVar(...) 写入 int、bool、float、string、Vector2、Vector3、Quaternion,客户端和服务端用 TryGetSyncVar(...) / HasSyncVar(...) 读取。SetSyncVar(...) 只在服务端 SDK 暴露;客户端需要改变同步状态时应请求服务端处理。SyncVar 适合节点级元数据或小型状态,不替代 Entity / Unit / Player 属性系统,也不要存复杂对象或大 payload。
SCE 场景里的 replicated Node 默认随当前 SCE Scene 同步;如果 Node 通过 UnitNodeBinding 绑定到 Unit,则客户端生命周期会跟随该 Unit 的服务端视野可见性。Unit 离开某客户端视野时,该客户端收到绑定 Node 的 OnNodeUnreplicated 且不再接收 transform/delta;Unit 重新可见时重新收到 OnNodeReplicated 和完整 transform。该语义覆盖 UnitDriven、NodeDriven 和 Manual 绑定;未绑定 Unit 的 node 不会按自身坐标自动套用单位视野。
ActorScope 可同步表现速记
服务端现在可以基于已有 ActorScope 创建或声明客户端表现。完整样例见 docs/systems/ServerActorPresentationSystem.md。生成代码时不要再假设“Actor 只能客户端创建,所以服务端只能发自定义消息”;优先让所有可同步表现从属于某个作用域。普通持久表现生成 scope.SetActorSlot(...),不要先手写 EnsureNodeBinding(...)。命名空间通常需要 using GameCore.ActorSystem; 和 using GameGraph.NodeSystem;。
| 需求 | 生成代码时优先使用 |
|---|---|
| 命中火花、爆炸、短音效、临时提示圈 | IActorScope.PlayTransient(actorLink, options) |
| 固定场景点上的持久提示 / 采集点 / 机关标记 | new ActorScopePersist(new ScenePoint(...), GSC.GameDataActorScope.Default) + scope.SetActorSlot(...) |
| 已有单位身上的持续光环、头顶表现、普通挂接表现 | 复用单位的 ActorScopePersist,再 scope.SetActorSlot(...) |
| 需要真实 replicated Node:hitbox、raycast target、GameGraph 组件、Node SyncVar 元数据 | scope.EnsureNodeBinding(...) + binding.SetActorSlot(...) |
| 客户端重新可见或 late join 后也要看到当前表现 | 持久 scope / actor slot,不要用 transient |
| 只用于本地预览、UI 或纯客户端工具 | 客户端直接创建 Actor / GameGraph 表现,不走服务端同步 |
PlayTransient 规则:
- 这是服务端入口,会请求当前可见客户端创建并播放一次性 actor;不 late join replay,也不会给当时不可见的客户端补播。
actorLink.Data.IsTransient == true时可省略Duration;如果 actor 数据不是 transient,必须提供ActorScopeTransientPlayOptions.Duration,运行时会强制按 transient actor 创建并按该时长清理。LocalOffset、Rotation、Scale作用在本次 actor 上;ScopeScale和Facing作用在本次 transient scope 上。- 不要自己用
Game.Delay手写销毁计时;返回false时应检查 scope context、actor 数据和非 transient actor 是否提供了Duration。
持久 actor slot 规则:
- 固定场景点的持久表现不要手写
IActorScopeContext。直接用ScenePoint创建 scope:var scope = new ActorScopePersist(new ScenePoint(position, scene), GSC.GameDataActorScope.Default);,然后scope.SetActorSlot(actorLink, key, options)。 - 已有 Unit / Entity 身上的持久表现优先复用它已有的
ActorScopePersist,不要额外创建一个脱离宿主的 scope。常见写法是var scope = owner.ActorScope as ActorScopePersist ?? owner.InitializeActorScope() as ActorScopePersist;。 - slot actor 必须是客户端和服务端都能解析到的非 transient
GameDataActor;一次性表现用PlayTransient,不要塞进持久 slot。 - 如果用代码定义 slot actor,
GameLink<GameDataActor, ...>、GameDataParticle、GameDataActorParticle等 link 声明和Game.OnGameDataInitialization初始化必须是双端代码,不要包在#if SERVER。服务端只同步 actor link,客户端必须能用同一个 link 查到本地GameDataActor才能创建表现。 - 如果 actor 来自数据编辑器生成的
ScopeData,直接使用该双端可见 link;不要在服务端临时 new 一个只有服务端知道的GameDataActor。 scope.SetActorSlot(...)默认把 Actor 附着到 scope 自身AttachSite。它内部使用默认 root binding 承载同步声明和 version 刷新,但调用方不需要管理 nodebinding。ActorScopeNodeBinding只作为高级能力,面向需要真实 replicated Node 的场景。ActorScopeNodeBindingRole是(Role, Key)查找标识,不是层级、骨骼或 socket;BindingNodeattach 的目标是另一个 binding Node,不是另一个 slot actor 的 visual node。SetActorSlot、ClearActorSlot、ClearActorSlots会自动推进 scope 的 actor slot version,用户不需要手动加版本号或写刷新变量。- 绑定 node 从属于
ActorScopePersist,客户端生命周期跟随作用域可见性;客户端看不到该作用域时,也不会收到相关 node 和 slot 声明。这是服务端可同步表现的默认反作弊和限流边界。 - 客户端的
GetActorSlotActor(...)、GetActorSlotActors()、RefreshActorSlots()只用于排查或精细控制,不要把客户端 actor 状态当作权威玩法状态。 - 只有需要自定义
Player、Target、Source、Facing等高级上下文时,才考虑实现IActorScopeContext。不要把它作为普通持久表现样例的默认写法。
场景视野速记
需要战争迷雾、视野全开、精确视野或扇形视野时,优先读 docs/systems/SceneVisionSystem.md。对 AI 助手来说,默认按下面分层生成代码:
- 场景级入口是
GameDataScene.Vision,按 scene 生效;SceneVisionMode.Normal是正常视野,Open是视野全开,Precise是距离 + 阻挡 + 可选扇形角度过滤。 - 精确视野的观察者策略是
VisionObserverPolicy.AllOwnedUnits、Explicit、MainControlledUnit。单位很多、只想让少量单位提供视野时优先用Explicit,并给观察者单位配置ObserverEnabled = true。 - 单位默认入口是可选的
GameDataUnit.Vision = new UnitVisionConfig { ... }。没有配置时不要强行创建UnitVision;该配置目前只影响精确视野下的观察者选择、圆形/扇形形状和本地遮罩表现。 - 视野距离始终来自
GSC.GameDataUnitProperty.Sight。不要在UnitVisionConfig里编造Range字段;运行时unit.Vision.Range读写的也是Sight,服务端可写,客户端只读。 - 扇形视野使用
Shape = UnitVisionShape.Cone、CentralAngle < 360、可选InnerRadius和DirectionSource。InnerRadius不是视野距离。 - 客户端有现成可用的本地迷雾遮罩渲染,由
Player.LocalVisionFocus决定,同一客户端同一时间只支持一个焦点,通常是主控单位。开启SceneVisionConfig.UseMainUnitAsLocalVisionFocus后,会自动回退到本地玩家MainUnit;手动设置LocalVisionFocus后不再自动回退,调用ClearLocalVisionFocusOverride()恢复。 - 单焦点只是内置客户端渲染的限制,不是服务端视野机制限制。服务端精确/扇形视野可以由多个观察者共同提供;如果客户端需要同时显示多个单位的圆形或扇形视野形状,建议用 Canvas 或 GameGraph 绘制 API 自绘表现,不要直接调用低层视野导入接口。
- 精确视野比普通视野更贵;优先把观察者数量控制在少量关键单位上。几十到一两百个通常更稳,达到几百个应压测;避免让成百上千个小怪都成为扇形精确视野观察者。
- 玩家单位少、敌方怪物多且怪物不需要遵守迷雾/墙体/扇形时,可以不给怪物配置
GameDataUnit.Vision,改自定义怪物GameDataAIThinkTree.ScanFilters去掉UnitRelationship.Visible,让怪物按 AI 扫描范围发现敌人。若有潜行玩法,优先用自定义扩展UnitState,并在 AIScanFilters、技能AcquireSettings.TargetingFilters、必要的GameDataEffectSearch.SearchFilters中排除该状态;复用UnitState.InvisibleToEnemy时要额外确认侦测/反隐语义。 - 服务端可见性是权威来源;客户端遮罩只是表现。不要直接调用低层视野导入接口,也不要把技能扇形范围误写成场景视野。
服务端权威 3D 物理速记
3D 物理 API 已双端可用。非 Unit 的服务端权威物理对象推荐使用 replicated Node 同步 transform,PhysicsWorld / RigidBody / CollisionShape 默认保持服务端 local;客户端在 replicated 节点上补本地 StaticMesh、材质、粒子、音效等表现。静态障碍优先双端 local,只有运行时状态变化才同步 replicated 节点、节点 SyncVar 或事件。
需要按玩家隔离或一个客户端同时接收多个临时图时,用脚本创建的 SceneGraphReplication.ReplicatedUnpublished 并在服务端 Publish(player)。完整模式和 smoke 测试结论见 docs/ai/skills/server-authoritative-3d-physics/SKILL.md、docs/systems/PhysicsSystem.md 与 docs/systems/ServerGameGraphSceneGraphReplication.md。
项目资源路径速记
引擎资源路径和脚本文件读写路径不同,生成代码时先判断使用场景:
| 场景 | 文件位置 | 代码路径 |
|---|---|---|
UI 图片 / Canvas Image / 图标 / 精灵图 | ui/image/abc.png | image/abc.png |
official-icons.json 中的官方图标素材 | ui/icon/buff/_0000_水晶能量.png | icon/buff/_0000_水晶能量.png |
| UI Spine | ui/Spine/hero/hero.skel + .atlas + .png | Spine/hero/hero |
| GameGraph 2D 动画(Spriter / Spine) | Spriter/序列帧放 ui/image/Animated/coin.scml,Spine 放 ui/spine/hero/hero.skel;旧项目可继续用 res/... | AnimationSet2D.Load("image/Animated/coin.scml") 或 AnimationSet2D.Load("spine/hero/hero.skel") |
| 未上传云但要随包的模型/粒子/音频等 | res/effect/custom/particle.effect 或 res/sound/custom/click.wav | effect/custom/particle.effect 或 sound/custom/click.wav |
脚本直接 File.ReadAllText 读取的普通文件 | ui/AppBundle/user_files/config.json 或 AppBundle/user_files/config.json | user_files/config.json |
PC 和手机发布包都可能将项目图片转换为 KTX 等运行时纹理容器,即使路径仍以 .png / .jpg 结尾。不要用 GameGraph.ResourceSystem.Image.Load(...) 的 CPU 解码结果作为 GameUI/Canvas 图片的存在性检查或绘制前提;CPU 读取器可能不支持发布格式。普通绘制使用逻辑图片路径;确需预加载独立散图做失败回落时,用 GameGraph.ResourceSystem.Texture2D.Load(path),现有客户端内部经 TextureX 取得 Texture2D。该散图检查不适用于图集虚拟子图,也不应每帧执行。示例、旧客户端边界及发布排查见 docs/guides/ResourcePathGuide.md#发布图片的加载与检查。
移动端发布会把项目 ui/ 下可识别的普通图片转为 GPU 压缩纹理(UI 通常为 ASTC 4×4);目前没有受支持的单图保留原始 PNG 开关。不要假设发布图片可以直接做 CPU 像素处理。在 Android/iOS 手机环境,不要对项目发布的 UI 图片调用 GameGraph.ResourceSystem.Image.Resize(...):Image.Load(...) -> GetSubimage(...) -> Resize(...) -> Texture2D.CreateFromImage(...) 即使在编辑器源图下正常,也不能证明发布后可用,压缩图片不能直接 Resize。素材裁剪、缩放应在发布前完成,单纯显示尺寸用控件布局或 Canvas.DrawImage 的目标尺寸;运行时新建的未压缩 Image 不受此限制。详见 docs/guides/ResourcePathGuide.md#移动端发布后的图片格式。
刷新资源前先区分缓存:GameUI 控件图片和 Canvas.DrawImage 的普通散图属于当前 GameUI 视图的图片缓存,不用 ResourceCache 的 Image 或 Texture2D 类型名释放。同一 PIE 中替换原有的项目 ui/ 散图后,调用 GameUI.ImageCache.Invalidate("image/abc.png");正常停止并重新启动 PIE 时,此类项目图片缓存会自动失效。ResourceCache 的 Image 指 GameGraph CPU 图片,Texture2D 指引擎纹理(包括 Spine atlas 页);Spine、图集、打包图片集合不属于普通 GameUI 散图的刷新保证。详见 docs/guides/ResourcePathGuide.md#开发中替换同路径图片 和 docs/systems/GameGraphOverview.md#resourcecache-常用资源类型名客户端。
不要把引擎图片、Spine 或音频放到 user_files,也不要在 Image 路径里写 ui/ 前缀。UI Spine 和 GameGraph Spine 路径规则不同:UI Spine.Resource 不写 .skel 后缀,GameGraph AnimationSet2D.Load(...) 必须写 .scml、.skel 或 .json 扩展名;AnimationSet2D 的 UI 路径同样不写 ui/ 前缀,并会按依赖库优先级查找,找不到再 fallback 当前项目 res/...。resources/ui-images.json 来自 GameSystemUI,可以直接引用;需要先看原图时,从编辑器下载缓存 <EditorUpdateRoot>/res/_m/maps/gamesystemui/<version>/gamesystemui/ui/{path} 读取,多个版本优先看最大版本号。resources/official-icons.json 中的图标必须先复制到项目 ui/icon/...,代码或数据里仍写 icon/...;需要先看原图时,从 <EditorUpdateRoot>/res/icon/{sourceRelativePath} 读取。如果复制源不存在,告知用户打开编辑器美术资源库,点击左侧「图标」节点,等待右侧缩略图正常显示后再复制。自定义音频放在项目 res/sound/...,代码写 sound/...;SoundResource.Load 的参数类型是 GameCore.ResourceType.Sound,Sound 支持从 string 和 UTF8String 隐式转换;支持 WAV/OGG,扩展名按实际文件填写。需要发布统计的自定义音频,确保同一路径也出现在 GameDataSound.Asset 或其他 Sound / IFile 类型字段中,并优先直接写字面量路径,避免路径只出现在 SoundResource.Load(...) 代码调用里导致清单漏收。不要让 AI 助手只在运行时临时 new GameDataSound / new GameDataActorSound 批量注册音效;静态音频应优先进入可收集的静态数据或显式资源清单,轻量 helper 也必须在 Client-Resource 后确认 ref/objref.txt 已包含路径。自定义模型等资源通常由资源库上传到云端;只有特殊随包需求才放项目 res/。自定义模型、第一人称手臂、枪械视图或其他随包 3D 资源,如果不是云端资源库自动同步得到,就要进入可分析的数据链或显式资源清单,不能只依赖运行时字符串。完整说明见 docs/guides/ResourcePathGuide.md 和 docs/systems/GameGraph2DAnimation.md。
本地模板与依赖库源码缓存
优先使用项目内 docs/sdk/ai/skills/、docs/sdk/systems/、docs/api/ 和 docs/resources/。当 API stub 或系统文档不足以判断默认数据、控件实现或完整示例时,可以读取编辑器下载缓存中的明文源码/数据:
| 项目/库 | 职责 | 何时查看源码/数据 |
|---|---|---|
map_templates | 编辑器新建项目模板和完整示例集合;提供 game_entry 空项目、game_lib 库模板、data_sample 数据示例、code_sample 玩法示例。它不是运行时依赖库。 | 需要选模板、搭项目骨架、参考完整示例写法时,先看项目内 docs/examples-index.md,再按索引读其头部给出的 map_templates/code_sample/src/ 子目录。示例可能滞后;与当前 skill 冲突时以 skill 为准。 |
GameSystemUI | 通用 UI/输入依赖库;提供移动摇杆/键盘适配、技能摇杆、商店、背包、Buff/聊天等 UI 系统,以及 resources/ui-images.json 可直接引用的内置 UI 图片。 | 需要理解控件生命周期、默认 UI 配置、图片来源或现有 UI 系统实现时查看:<EditorUpdateRoot>/res/_m/maps/gamesystemui/<version>/gamesystemui/,例如 E:/WasmSCE/update/editor-master.spark.xd.com/res/_m/maps/gamesystemui/114/gamesystemui。 |
GameSparkCore | 通用玩法数据与辅助逻辑依赖库;提供框架预置 ScopeData(属性、生命值、伤害类型、Gameplay、玩家设置、AI 预设、血条等)和部分扩展方法/系统辅助。项目通常通过 GSC = gamesparkcore.ScopeData 访问默认数据。 | 需要确认默认数据、预设参数或扩展方法实现时查看:<EditorUpdateRoot>/res/_m/maps/gamesparkcore/<version>/gamesparkcore/,例如 E:/WasmSCE/update/editor-master.spark.xd.com/res/_m/maps/gamesparkcore/163/gamesparkcore。常看源数据目录 editor/data/gamesparkcore/ScopeData/、editor/data/gamesparkcore/ScopeDataTemplate/,以及生成代码 src/DataGenerated/gamesparkcore/ScopeData/。 |
如果同一库下有多个数字版本目录,优先读取版本号最大的目录。下载缓存只作为本地只读参考;不要把这些绝对路径写进项目代码、数据或资源路径,也不要直接修改缓存里的依赖库文件。
AI 两层职责速记
概念定义以 AI 系统 为准;做排障时可以先按下面的职责边界判断,不要把 WaveAI 和 TacticalAI / AIThinkTree 混在一起。
| 现象 / 目标 | 优先看什么 | 说明 |
|---|---|---|
| 单个怪会不会自动扫描、攻击、施法 | TacticalAI / AIThinkTree | GameDataUnit.TacticalAI 是配置字段,运行时组件名叫 AIThinkTree |
| 一波怪沿路巡逻、跟随首领、主动接近玩家 | WaveAI | 负责宏观移动、路线和接近逻辑 |
| 怪物会走但不打 | AIThinkTree 是否真的已挂上 / enableCombat | 先查单位最终有没有战斗 AI,再查 WaveAI 战斗开关,最后才查 scan range、MinimalApproachRange、HysteresisFactor(Follow)/ ReacquireRangeFactor(Hunt) |
AI 战斗排查顺序(会动但不打先看这里)
如果现象是“怪物会巡逻、跟随或追人,但就是不攻击”,不要先猜 scan range、MinimalApproachRange 或 HysteresisFactor(Follow)/ ReacquireRangeFactor(Hunt)。这类问题优先按下面顺序排查:
先查单位最终是否真的挂上了战斗 AI
csharpvar enemy = enemyLink.Data?.CreateUnit(player, pos, facing, useDefaultAI: true); AIThinkTree.AddDefaultAI(enemy!); // 或者: // myAIThinkTreeLink.Data!.CreateAI(enemy!);对依赖
GameDataUnit.TacticalAI的战斗单位,CreateUnit(..., useDefaultAI: false)只表示创建时不自动创建AIThinkTree,不代表之后不能动态添加。
如果既没有在创建时传useDefaultAI: true,也没有后续调用AIThinkTree.AddDefaultAI(unit)或myAIThinkTreeLink.Data!.CreateAI(unit),结果通常就是:单位仍然能被WaveAI驱动移动,但不会进入单体战斗决策。再查 WaveAI 有没有在运行时把战斗关掉
csharpvar waveAI = patrolLink.Data!.StartWaveAI(units, enableCombat: true); waveAI.SetCombatEnabled(null); // 或 true waveAI.SetRoute(routePoints, enableCombat: true);下列写法都表示“行军但不战斗”:
StartWaveAI(..., enableCombat: false)waveAI.SetCombatEnabled(false)waveAI.SetRoute(..., enableCombat: false)
如果只是想沿用数编里的
EnableCombat,优先传null,不要手写false。然后再查战斗 AI / 攻击能力 / 目标过滤
GameDataUnit.TacticalAI已配置为可用的GameDataAIThinkTree(GSC.GameDataAIThinkTree.Default只是默认预设,也可以自定义)- 如果使用默认自动攻击方案,
Abilities中至少一个技能设置了IsAttack = true - 目标单位的
Filter命中当前 AI 的ScanFilters;默认 AI 攻击普通战斗单位时通常要求UnitFilter.Unit - 敌我阵营关系正确,不要把敌人放到中立玩家上
如果单位要攻击建筑,还要额外确认
AIThinkTree.ScanFilters/ 技能AcquireSettings.TargetingFilters是否允许Structure。最后才调扫描和接近参数
只有在前面的“硬开关”都确认正确之后,才去看:
MinimalScanRangeMaximalScanRangeMinimalApproachRangeHysteresisFactor(Follow)/ReacquireRangeFactor(Hunt)CombatLeash/CombatResetRange
否则很容易把“AI 根本没启用”误判成“扫描距离太小”。
GameData 边界与运行时修改(重要)
先区分“定义数编类型”和“创建/修改数编条目”:要使用图形化数据编辑器时,自定义类型应在触发编辑器 Class 的属性面板中选择“设置为数编分类”或“设置为数编类型”;只有明确的纯代码开发才直接手写 [GameDataCategory] / [GameDataNodeType<T,V>]。完整路线见 docs/systems/GameDataSystem.md 和 docs/ai/skills/data-editor/SKILL.md。
默认把 ScopeData.*.Data 视为双端共享的静态定义来源;如果这些条目来自数据编辑器,它们通常也对应 editor/data 下的 JSON 数编文件。
这一节不是禁止你在代码中动态创建或修改 GameData。引擎允许这样做,而且某些诉求本来就应该通过代码动态生成,例如:
- 在运行时创建带特定属性组合的单位或物品数编
- 为某个系统构造 runtime-only 的临时模板
- 按局内规则生成不需要落回数据编辑器的派生定义
真正需要避免的,是把静态定义、运行时模板和实例状态混写在一起,导致数据编辑器预览失去意义、代码和 JSON 同时改同一份定义、以及用户和 AI 助手都难以判断“这个值到底以哪边为准”。
推荐分层方式
- 静态定义:如果某份数据本来就应该长期存在、需要在编辑器里预览、调参、复用,优先写在数据编辑器 / JSON 中,不要再在运行时把同一份共享定义改成另一套逻辑。
- 运行时模板:如果某份定义本来就是局内动态生成、只服务某段运行时逻辑,那么可以在代码中创建独立的 runtime-only
GameData对象,而不是去补丁已有的共享表。 - 实例状态:如果变化只影响某个已经创建出来的单位、物品或对象,优先修改实例本身,而不是回头修改其共享
GameData。
避免混乱的默认原则
- 不要在 gameplay trigger、刷怪逻辑或临时调试代码里直接修改
ScopeData.*.Data指向的共享GameData,尤其不要把模型、AI、同步方式、默认属性这类“单位定义”语义写成运行时补丁。 - 静态定义优先写回数编文件,或在
OnGameDataInitialization中创建独立的 runtime-onlyGameData对象;不要把“编辑器数据”和“运行时实例逻辑”混写在同一段刷怪代码里。 - 按波次成长、临时倍率、局内 Buff、出生后附加状态等动态变化,优先在
CreateUnit之后修改单位实例的属性,而不是回头改GameDataUnit。
需要刷新底层缓存的情况
部分 GameData 类型在运行时会被引擎底层缓存。相关类型实现 IGameDataNativeCacheRefreshable,可以在修改后调用 SetNativeCacheRefresh(),请求后续底层重新读取并更新这份定义。这个调用在客户端和服务端都可以使用;如果当前一侧没有使用对应缓存,会安全忽略该请求。
常见需要注意的类型主要包括:
GameDataUnitGameDataActorScopeGameDataActorGameDataModelGameDataParticleGameDataSoundGameDataAnimationSequence
这些类型的方法在双端都可以调用。实际是否会对当前平台产生刷新效果,取决于该平台是否使用了对应的底层缓存;未使用的一侧会安全忽略刷新请求。
GameDataUnit 是最需要注意的例子。它对应的单位定义在引擎底层会被缓存。如果你在运行中修改了共享的 GameDataUnit,例如改了默认属性、模型、碰撞或其他会影响单位定义的字段,但没有调用 SetNativeCacheRefresh(),那么后续新创建的同类单位仍可能沿用旧缓存生成;而已经创建出来的单位通常也不会自动回溯更新。
一个常见错误是:运行中把某个 GameDataUnit 的默认生命值或模型改掉,然后继续用同一个 UnitLink 刷怪,却没有刷新缓存。结果可能是你看到“代码里已经是新数值了”,但后续刷出来的单位仍带着旧表现或旧默认数据。更危险的是,如果这段修改只在服务端或只在客户端执行,或者两端字段值、执行时机不一致,那么同一个 UnitLink 在两端就可能代表不同的单位定义,导致服务端逻辑和客户端表现发生偏差。
双端一致性要求
如果确实必须补丁共享 GameData,必须通过客户端和服务端一致的初始化路径执行,并明确记录原因;不要只在服务端懒触发或只在某个触发器分支里单边修改。
不管是动态创建还是动态修改数编,都必须优先保证客户端和服务端的一致性,尤其是 GameDataUnit:
- 两端使用同一组
GameLink - 两端写入同一份字段值
- 两端在相同的初始化或运行时路径中执行相同的修改逻辑
- 修改会影响底层缓存结果的共享数编后,及时调用
SetNativeCacheRefresh()
不要把“只在某个触发器分支里临时改一下单位表”当成安全做法,也不要假设同步系统会自动修正两端不同的 GameDataUnit 定义。
换句话说,推荐的优先级是:
- 需要编辑器可见、可预览、可复用的定义,放进数据编辑器 / JSON
- 需要局内动态生成的一份新模板,用代码创建新的
GameData - 只影响单个对象的临时变化,修改实例而不是共享表
- 只有在确实要修改共享定义时,才同时处理双端一致性和
SetNativeCacheRefresh()
玩家文本合规
- 聊天、私聊、昵称、公会名、房间名、留言和其他用户生成文本在展示、保存或转发前,必须在服务端调用
Game.FilterSensitiveWords。 DualEndEvent、TypedMessage、ProtoCustomMessage和ServerChannel都只负责消息传输,不会自动过滤玩家文本。ServerChannel的 channel 是发布/订阅消息主题或路由标识,不是玩家聊天房间。使用它开发跨服聊天时,先过滤文本,再发布FilteredText。- 生成或审查相关代码前,先读
docs/ai/skills/sensitive-word-filter/SKILL.md和docs/systems/SensitiveWordFilterSystem.md。
技术约束
由于 WebAssembly 环境限制,以下 API 禁止使用:
| 禁止的 API | 替代方案 |
|---|---|
Task.Run() | 不需要,单线程 |
TaskCreationOptions.RunContinuationsAsynchronously | new TaskCompletionSource<T>() |
Task.Delay() | Game.Delay() |
Thread 相关 API | 不支持多线程 |
Console.WriteLine | Game.Logger.LogInformation() |
游戏逻辑应在服务端实现以避免作弊,使用框架的同步机制。
时间 API
| API | 返回类型 | 用途 |
|---|---|---|
Game.Time | TimeSpan | 游戏启动后的总时间,用 .TotalSeconds 或 .TotalMilliseconds 取数值 |
Game.ElapsedTime | TimeSpan | 上一帧到当前帧的时间差(deltaTime),用 .TotalSeconds 取浮点秒数 |
Game.TotalElapsedTickInMilliseconds | int | 游戏启动后的总毫秒数(整数),适合计时、超时判断 |
Game.Delay(ms) | Task | 异步等待指定毫秒数(替代 Task.Delay) |
csharp
// deltaTime(每帧时间差)
float dt = (float)Game.ElapsedTime.TotalSeconds;
// 绝对时间戳(判断超时、冷却)
int now = Game.TotalElapsedTickInMilliseconds;
if (now - lastActionTime > cooldownMs) { /* 冷却结束 */ }
// 异步等待
await Game.Delay(3000); // 等待 3 秒ScopeData(数据编辑器生成的公开 Link)
数据编辑器中创建的公开数据条目会被代码生成器生成到 <库名>.ScopeData 命名空间中,以静态 GameLink 字段的形式暴露。
命名规则
<库名>.ScopeData.<数据类型名>.<条目名>| 来源 | 命名空间 | 示例 |
|---|---|---|
框架预置库 gamesparkcore | gamesparkcore.ScopeData | GameDataUnitProperty.LifeMax, GameDataVital.Health |
用户游戏项目 GameEntry | GameEntry.ScopeData | GameDataScene.new_scene, GameDataCamera.DefaultCamera |
自定义库 MyLib | MyLib.ScopeData | GameDataUnit.CustomUnit |
每个数据类型对应一个同名静态类(如 GameDataUnit、GameDataUnitProperty),其中包含该类型所有公开条目的 GameLink 字段。
GSC 别名
gamesparkcore.ScopeData 中的静态类名与框架基类同名(如两者都有 GameDataUnit),直接 using 会产生歧义。因此 csproj 中预置了全局命名空间别名:
csharp
// csproj 中已配置: global using GSC = gamesparkcore.ScopeData;
GSC.GameDataUnitProperty.LifeMax // 生命值上限属性
GSC.GameDataVital.Health // 生命值 Vital
GSC.GameDataDamageType.Physical // 物理伤害类型
GSC.GameDataGameplay.Default // 默认 Gameplay 配置
GSC.GameDataPlayerSettings.Default // 默认玩家设置
GSC.GameDataGameUI.Default // 默认游戏 UI用户项目自己的 ScopeData(如 GameEntry.ScopeData)通常不冲突,可以直接 using。但如果项目引用了自定义库,库的 ScopeData 也可能包含同名静态类(如 MyLib.ScopeData.GameDataUnit),此时同样需要为库的 ScopeData 设置别名以避免歧义。
常用框架预置数据
| 类别 | 访问方式 | 说明 |
|---|---|---|
| 单位属性 | GSC.GameDataUnitProperty.LifeMax | 生命上限、攻击力、护甲等 |
| 生命值 | GSC.GameDataVital.Health / .Mana | Vital 组件标识 |
| 伤害类型 | GSC.GameDataDamageType.Physical / .Magical / .Pure | 伤害系统分类 |
| 游戏模式配置 | GSC.GameDataGameplay.Default | Gameplay 默认配置 |
| 玩家设置 | GSC.GameDataPlayerSettings.Default | 默认玩家设置 |
| AI 思维树 | GSC.GameDataAIThinkTree.Default | 框架内置默认战斗 AI 预设,可替换为自定义 GameDataAIThinkTree |
完整列表见 api/server/gamesparkcore_ScopeData.cs。
必读框架资料
docs/FRAMEWORK_OVERVIEW.md- 框架概述docs/guides/QuickStart.md- 快速开始指南docs/guides/ResourcePathGuide.md- 图片、Spine、res/随包资源与user_files的路径规则docs/systems/DisposableObjectSystem.md- DisposableObject 生命周期、资源清理与自定义派生类扩展docs/systems/GameGraph2DAnimation.md- GameGraphStaticSprite2D/AnimatedSprite2D、Spriter / Spine 和 3D 场景中的 2D 动画docs/best-practices/CommonPitfalls.md- 已知错误写法与修正docs/systems/- 各系统详细文档docs/ai/skills/wasicore-game-architecture/SKILL.md- 游戏架构路线与Surface2D/Volumetric3D概念澄清docs/ai/skills/canvas-2d-game/SKILL.md- Canvas / CanvasAnimated 绘制型游戏开发docs/ai/skills/3d-unit-game/SKILL.md- SCE Unit/技能系统游戏开发(Unit、Vital、移动、跨端通信)docs/ai/skills/ui-layout-api/SKILL.md- 流式布局 UI API
坐标系统(3D 游戏)
WasiCore 使用类似 Unreal Engine 的坐标系,Z 轴是高度轴:
- X 轴:水平方向(左右)
- Y 轴:水平方向(前后)
- Z 轴:高度方向(上下)
- XY 平面:地面
csharp
var position = new Vector3(x, y, height); // Z 是高度
velocity.Z -= gravity * deltaTime; // 重力在 Z 轴负方向
velocity.Z = jumpForce; // 跳跃在 Z 轴正方向
if (position.Z <= 0) { position.Z = 0; IsOnGround = true; }详见:docs/COORDINATE_SYSTEM_GUIDE.md
架构设计
- Entity 处理逻辑:游戏状态、属性、同步
- Actor 处理视觉:模型、特效、动画、声音
- 服务端权威:游戏逻辑在服务端以避免作弊
UI 布局
优先使用流式布局模式,详见 docs/ai/skills/ui-layout-api/SKILL.md。
csharp
var panel = new Panel {
FlowOrientation = Orientation.Vertical,
Height = AutoMode.Auto,
Padding = new Thickness(16, 12, 16, 12)
};
var titleLabel = new Label {
Text = "标题",
Parent = panel,
Margin = new Thickness(0, 0, 0, 12)
};日志与自动注册
- 使用
Game.Logger记录运行上下文,例如Game.Logger.LogInformation("消息: {Parameter}", value)。 - 需要自动接入生命周期的类实现
IGameClass,在public static void OnRegisterGameClass()中订阅后续事件。 - 命名、排版和代码组织沿用游戏项目自己的约定。
游戏模式
每个项目已有一个 MapGameMode,它与地形编辑器和玩家队伍设置编辑器联动,已包含场景、玩家配置等完整设置。直接使用它,不需要新建游戏模式,也不需要修改 GlobalConfig.cs。 除非有在一个项目中使用多个游戏模式的明确需求,否则不需要新建游戏模式。
csharp
public class MyGame : IGameClass
{
public static void OnRegisterGameClass()
{
Game.OnGameTriggerInitialization += OnGameTriggerInitialization;
}
private static void OnGameTriggerInitialization()
{
// 使用项目已有的 MapGameMode(已在 GlobalConfig 中注册为默认测试模式)
if (Game.GameModeLink != ScopeData.GameDataGameMode.MapGameMode) return;
Game.Subscribe<EventGameStart>(async (s, d) =>
{
// 加载场景(MapGameMode 已配置 new_scene)
var scene = Scene.GetOrCreate(ScopeData.GameDataScene.new_scene);
scene.Load();
// ... 游戏逻辑
});
}
}> HostedSceneTag 对应编辑器中创建的地形资产,不可在代码中编造。使用 ScopeData.GameDataScene.* 引用已有场景。
开发检查清单
编译和环境
- [ ] 使用正确的编译配置:
-c Server-Debug或-c Client-Debug - [ ] 客户端代码包裹在
#if CLIENT中 - [ ] 服务端代码包裹在
#if SERVER中
游戏模式
- [ ] 使用项目已有的
ScopeData.GameDataGameMode.MapGameMode(与地形编辑器和玩家队伍设置联动,除非明确需要多个游戏模式,否则不要新建) - [ ] 不要修改
GlobalConfig.cs(MapGameMode 已注册为默认测试模式) - [ ] 游戏模式判断放在
OnGameTriggerInitializationhandler 中(Game.GameModeLink在OnRegisterGameClass阶段尚未赋值) - [ ] 需要 SCE 场景的游戏通过
Scene.GetOrCreate(ScopeData.GameDataScene.new_scene)+Load()加载;Canvas-only 游戏模式可不加载场景
运行时约束
- [ ] 使用框架日志系统:
Game.Logger.LogInformation("消息: {Param}", value) - [ ] 避免禁用 API:
Task.Run(),TaskCreationOptions.RunContinuationsAsynchronously,Task.Delay(),Console.WriteLine - [ ] 使用正确替代 API:
Game.Delay(),Game.Logger - [ ] 实现了
IGameClass接口(如需自动注册) - [ ] DualEndEvent 事件数据使用
public get; set;属性,不使用字段传 payload
3D 坐标系统(如涉及)
- [ ] Z 轴用于高度(而非 Y 轴)
- [ ] 重力作用在 Z 轴负方向
玩家与队伍(如涉及敌我判定)
- [ ] 查看项目
PlayerSettings.cs确认队伍配置(不要假设固定的队伍结构) - [ ] 中立玩家(
IsNeutral=true,默认为玩家 0)的单位永远不会被判定为Enemy - [ ] 使用
Player.GetRelationShip(other)判定玩家关系(详见 PlayerSystem.md)
架构设计
- [ ] Entity 处理逻辑,Actor 处理视觉
- [ ] 服务端权威
常见问题
编译错误
首先检查:编译配置是否正确 → 是否使用了禁用 API → 项目引用 → 条件编译指令
运行时错误
检查 IGameClass 接口 → 异步操作方式 → 网络同步配置 → 编辑器安装目录下的 logs/ 根目录、logs/client 与 logs/server
更多详细信息
完整文档见 reference.md。