主题
Client SceneGraph Game
Use this skill when the task is a local 3D mini-game or preview built directly with SceneGraph, Node, PhysicsWorld, meshes, materials, camera, GameUI, and GameSystemUI input. Do not use it for authoritative multiplayer physics; use server-authoritative-3d-physics instead.
This is the direct GameGraph route. It does not provide SCE Unit, Ability, Buff, AI, HP, orders, or targeting. If the user needs those systems, use 3d-unit-game instead. If the user is still deciding between SCE Unit, direct GameGraph, and Canvas, read wasicore-game-architecture first.
Detailed human-facing notes: docs/best-practices/ClientSceneGraphGamePitfalls.md.
For character equipment, clothing, or other skeletal appearance additions, read GameGraph 附加模型挂载. This is the GameGraph drill-down interface of 自动挂载模型表现: it reuses the mounting, bone-following, and optional host-mesh culling mechanism, while project code selects the Prefab main skeletal host and owns the attachment lifetime. Existing ActorArray-driven Unit/item appearances remain on the Actor route.
When converting a local shooter to server authority or adding cooperative players, read shooter-authoritative-sync for input uplink, independent body/aim replication, prediction, and remote smoothing. Keep client-local presentation separate from authority.
For a playable game, apply game-visual-production alongside this API workflow. map_templates/gamegraph_top_down_pve_template is a worked reference for a themed arena, articulated mechanical characters, 3D projectiles, enemy spawning, and visible death/restart. It now uses server-authoritative cooperative gameplay with client-local presentation; follow shooter-authoritative-sync for that multiplayer route. Its static-part animation, camera, and lighting are example choices rather than contracts for other games.
For Unity/Urho or map-editor experience transfer and low-level extensions inside an existing SCE project, use the references in wasicore-game-architecture. Preserve suitable Unit/data/trigger systems when adding a local engine feature; user experience is an explanation aid, not a reason to replace the project's architecture.
Urho-Compatible Mental Model
GameGraph primarily exports and wraps commonly used gameplay APIs from Spark's Urho engine as C# interfaces, adding Spark-specific capabilities and GameCore bridges. Use existing Urho knowledge to find implementation approaches through the familiar SceneGraph → Node → Component, transform, Serializable, ResourceCache, Drawable, Octree, and PhysicsWorld model, then confirm exact types, signatures, and availability in api/client / api/server.
The shared Urho compatibility and extension reference is GameGraph overview. In a generated project, use docs/sdk/systems/GameGraphOverview.md if the relative link is unavailable. Its guidance also applies to server APIs and Unit-project extensions; this skill focuses on client-local development.
Do not assume a complete one-to-one Urho binding:
- Spark uses a Z-up left-handed coordinate system.
- Scene graph, Serializable, math, and core physics favor dual-end support. Resource contents, GPU/rendering, materials, audio resources and actual output, particles, and presentation animation are mostly client-only; some
SoundSource/SoundListenercomponent state and controls are shared. - Prefer GameCore
Unit,Actor,Scene, andPropertyObjectsemantics, then use typed bridges such asGetNode(),GetSceneGraph(),GetText3D(),GetAnimatedModel(), orGetAnimationController()for low-level control. - Spark particles and skeletal animation preserve familiar usage patterns but provide Spark-specific resource pipelines and capabilities. Follow the SDK,
runtime-particle-builder, andGameGraphSkeletalAnimation.mdrather than assuming native Urho behavior. - For skeletal models, choose the skinning path explicitly when a view contains the same character as another camera:
SimpleSkinning = trueselects ordinary hardware skinning;SimpleSkinning = falsewithComputeShaderSkinning = falseselects CE texture skinning;SimpleSkinning = falsewithComputeShaderSkinning = trueselects Compute Shader skinning.SimpleSkinningrequires client capability version3; the wrapper gates the new import on old clients. Simple skinning is the practical short-term workaround for picture-in-picture/minimap rendering because the texture-atlas and compute output state is not yet fully View-local. This is a model-level switch, not a per-View isolation mechanism. Text3DComponentis a scene drawable; use GameUI for normal UI. Navigation usesSceneNavigation/ Nav3D rather than a direct export of every low-level Urho navigation type.- C++ mutating math methods may be exposed as value-returning C# methods. Follow the C# signature.
Fit Check
| Goal | Use |
|---|---|
| Single-player 3D mini-game, local tool, material/model probe | Client local SceneGraph |
| 2D image, Spriter frame animation, or Spine character that must live in a 3D scene | AnimatedSprite2D / StaticSprite2D in client SceneGraph |
| Unit combat, skills, AI, HP, navigation | 3d-unit-game |
| Networked moving rigid bodies or anti-cheat physics | server-authoritative-3d-physics |
| Canvas-only casual game | canvas-2d-game |
Client local SceneGraph is local presentation/gameplay only. It does not publish, sync, persist, or become server-authoritative by itself.
Startup Pattern
- Put all rendering/input code under
#if CLIENT. - Add a dedicated GameMode for samples that live beside other samples.
- Gate callbacks by
Game.GameModeLink. - Start after one frame with
await Game.Delay(TimeSpan.Zero)when creating UI, SceneGraph, or Viewport fromGame.OnGameStart. - Do not treat
GameStartor a one-frame delay as proof that the client SCE scene is loaded. For map-dependent work, use the configuredScene, checkScene.Loaded/Scene.LoadState, and wait forScene.OnLoadEnd. Game.LocalSceneis intentionally non-null before a configured map is available. WhenIsEngineFallbackis true, it is only a scene context for actors that do not require map data; do not query terrain, navigation, scene collision, placed objects, or the SCE SceneGraph from it.- On end/dispose, remove UI controls, unsubscribe input/events, restore viewport if needed, and dispose the local
SceneGraph. - Preserve the project's native Host startup configuration when making a client-local template. Normal editor debugging still starts a server before client gameplay. For a WASM mode that loads its own configured scene, keep
table/mapinfo.iniwithbAutoLoadScene = false; otherwise a missing file defaults to loadingscene/defaultbefore WASM initialization.config.ini'sauto_load_scenedoes not replace this setting. Keep the mode's actual SceneList/DefaultScene and player settings valid, then verify normal debug separately from a client-only preview.
Minimal gate:
csharp
if (Game.GameModeLink != MySample.ScopeData.GameMode.MySample)
{
return;
}Rendering Checklist
Creating nodes is not enough. A directly rendered independent graph needs:
new SceneGraph(createOctree: true)orgraph.EnsureOctree()- visible mesh components
- materials known to render in the current SDK
- light and normals for lit materials
CameraComponentRenderer.SetupMainViewport(graph, camera)or a deliberate custom viewport/render-target path- sane clip planes, node positions, and scales
For generated meshes, write normals. If a shader uses textures, also write UVs.
In embedded PIE, a native Viewport with a zero Rect converts pixels using the editor backbuffer dimensions, which may differ from the game's render surface. For a full-game-surface camera, use camera.GetScreenRay((float)x / gameWidthPx, (float)y / gameHeightPx) and multiply its normalized WorldToScreenPoint by the same game dimensions. Use the actual ScreenViewport and Canvas pixel rect for input/overlay conversion. If automatic camera aspect changes between input/UI work and rendering, maintain AspectRatio from the game surface and disable AutoAspectRatio for that camera. Custom sub-viewports need their own rect mapping. Verify both aiming and world-anchored markers in normal PIE.
Skeletal skinning path
When a character is rendered by both the main camera and a picture-in-picture/minimap camera, prefer the explicit three-way selection below until View-local skinning state is available:
csharp
#if CLIENT
var model = actor.GetAnimatedModel();
if (model != null)
{
model.SimpleSkinning = true; // ordinary hardware skinning
model.ComputeShaderSkinning = false;
}
#endifThe alternatives are:
SimpleSkinning = false; ComputeShaderSkinning = false: texture skinning, the usual CE non-Compute path; efficient for many bones but uses shared skinning-atlas state.SimpleSkinning = false; ComputeShaderSkinning = true: Compute Shader skinning; useful for large character/vertex workloads and requires platform GPU support, but its generated geometry is also shared at Drawable scope today.
Set both flags explicitly when switching modes. SimpleSkinning is model-wide and does not isolate one View from another. For a temporary minimap workaround, use SimpleSkinning or filter characters out of the minimap. The long-term View-local isolation work is tracked separately.
Troubleshooting quick check: if the same skeletal character tears or stretches only when the main camera and a minimap / picture-in-picture camera render it together, first confirm that both flags are set explicitly on every visible model part. Use SimpleSkinning = true and ComputeShaderSkinning = false as the short-term workaround; if the model exceeds the device bone/uniform limit, remove it from the secondary View instead. Do not treat this workaround as proof that the two Views are isolated—the long-term fix belongs in the renderer's View-local skinning state. The full symptom checklist is in GameGraphSkeletalAnimation.md.
When something is invisible, create a simple PBR witness object next to it before changing game logic. If the witness renders, the scene/camera/viewport path is probably sound and the suspect is material, shader, alpha, culling, stencil, render order, or geometry data.
Preserve a visual baseline during asset or lighting changes
- Save a working screenshot and the exact model/material paths, camera projection/clip planes, light setup, and postprocessing dependencies. Change one asset or rendering factor at a time and compare at the same window size and scene state.
- For a port, verify the source game's actual player, enemy, and weapon identities. A successful import of an unrelated test character does not verify the requested cast. Preserve articulated-part pivots; confirm axis, units, bounds, normals, material slots, and textures before applying animation. Use Maker model import for package import and humanoid workflow for rigged animation/retargeting.
- Move material adaptation, camera adjustments, light changes, and fullscreen effects into separate comparisons. An enabled effect can depend on a texture: enabling vignette without its mask made an otherwise visible 3D scene black in the top-down reference. Check the effect's resources before attributing the result to model import.
- Camera projection may change the actual lighting path. The top-down template ultimately uses long-focus perspective with cluster lights after an orthographic forward-point-light path showed polygonal boundaries. This is a tested example workaround, not a rule against orthographic cameras. When changing projection, update clipping, fog, shadow coverage, input mapping and viewport resize handling together. Verify local lights with an on/off comparison; replacing a GameGraph light with a scene-editor light may still reach the same native component.
Scene-space combat presentation
For visible travel in a 3D shooter, create a scene-space projectile mesh/effect with position, velocity, lifetime and hit feedback. Canvas remains useful for screen-space crosshairs, bars and radar; choose beams, hitscan tracers, sprites or 3D bodies by the intended weapon instead of drawing a debug line for every attack.
Keep presentation and hit authority explicit. Fast projectiles need a segment/swept query from previous to next position rather than an end-position-only overlap. Reuse projectile nodes/materials where appropriate, remove them on expiry/hit, and clear them on round end/reset. Slow enemy shots can provide the dodge warning themselves; additional lasers are a design choice. Verify moving enemies, projectile travel/hits, edge spawning and round-end behavior as well as player input.
Combat audio
For top-down or long-focus cameras, check the actual listener-to-source distance before choosing spatial attenuation. A listener attached to a distant camera can put every gameplay sound beyond FarDistance. Use a gameplay-plane listener when appropriate, or 2D sources with explicit gameplay-distance gain and camera-aware panning. Derive left/right from the actual camera rather than assuming world +X is screen-right. See GameGraph audio.
For rapid overlapping shots, use a bounded voice pool and a priority policy that leaves important hit/death feedback audible. Stop owned voices on reset and destroy owned sound nodes on disposal; avoid changing the global listener or master gain unless the feature owns that state and restores it. Project-local res/sound/... assets ship with the project, using sound/... code paths.
Resource readiness, playback counters and IsPlaying alone do not prove audible output. Inspect MixOutput, gain, panning and playback time, then listen to or capture actual output during gameplay; compare with an idle interval where practical.
Scene-Space 2D Animation
Use AnimatedSprite2D when the user wants a Spriter sequence or Spine animation to appear as an object inside a 3D SceneGraph. Do not render Spine to a Canvas/texture first unless the user specifically needs a texture pipeline; the direct GameGraph route keeps the object in scene-space camera, depth, and sorting flow.
- Load resources with
AnimationSet2D.Load(...); new project-local Spriter/image animations should live underui/image/..., Spine underui/spine/..., and the code path omitsui/but includes.scml,.skel, or.json. Old current-projectres/...paths remain a fallback. - Use
Skin/SetSkin(...)for Spine skins; useEntityfor Spriter entities. AnimatedSprite2Dis not an automatic billboard. It uses the node world transform; perspective cameras produce near/far scaling.- Default Urho2D materials are unlit: ordinary 3D lights and shadows do not illuminate sprites. Full-screen Bloom/tone mapping and Zone-driven color effects can still change their displayed colors; use
Color/Alphafor manual sprite tint. - Use
LayerandOrderInLayerfor 2D ordering. Depth interaction depends on the material and camera setup.
For sprite picking, ordinary Octree Aabb / Obb / Triangle / TriangleUv queries all hit the current world AABB, including transparent areas and gaps between Spriter/Spine parts. Keep the default DrawableQueryFlags.Any: the Octree entry is Renderer2D with Geometry flags, so Geometry2D alone misses it. Hits identify the sprite node/component, not a Spine bone/slot/attachment; UV is not computed and the normal is the negated ray direction. Use explicit gameplay hit shapes or physics colliders for precise combat; the visual component does not create them. See raycast semantics.
Complete references: docs/systems/GameGraph2DAnimation.md and map_templates/code_sample/src/AnimatedSprite2DSample/.
For TMX maps, load TmxFile2D and assign it to TileMap2DComponent in the graph. TMX does not create a camera. Use the same CameraComponent with IsOrthographic=true for a flat 2D view; the rendering projection and the GameSystemUI input-coordinate mapping are separate choices. The TmxMapSample camera looks along +Z at the XY map, so screen up maps to world +Y. See TMX maps for capability 7, compressed layers and the full sample.
The sample checks TmxFile2DModernLoading once: supported clients load Mossbay.tmx (1.11 + zlib), others load MossbayLegacy.tmx (1.0 + uncompressed Base64). Both carry the same gameplay data and share a tileset/image in the map directory. Choose by format capability, not by retrying every failed load; this fallback does not establish compatibility for unrelated APIs or render settings.
For a dedicated flat 2D view, load gamegraph-2d-rendering and the 2D rendering guide. Main-viewport binding retains the host RenderPath; an independent graph and unlit sprites do not isolate fullscreen postprocessing. The dedicated skill supplies XML paths and a viewport owner that saves/restores state, with separate linear PIE and Gamma-encoded standalone output. Keep mixed 3D and arbitrary RTT output contracts separate.
For finite orthogonal TMX maps, keep these coordinate contracts separate:
- The Tiled/TMX file stores pixel positions from the top-left, +X right, +Y down. The loader converts positions to map-local engine units from the bottom-left, +Y up; currently one pixel is 0.01 units. These are local coordinates, equal to world coordinates only when the map node's transform is identity.
TileMapObject2D.Position/Sizeare already converted. Rectangle positions refer to the lower-left corner after conversion. Do not rescale or flip them again. Tile lookup indices still count rows down from the top-left; they are not map-local positions.- GameSystemUI
Directionis a direction vector with screen axes, not a screen point. Its zero means centered/stopped, not the top-left UI origin. For the sample's unrotated XY camera, map(dx,dy)to(dx,-dy); do not apply the point formulamapHeight-yor pixel scaling to movement directions.
Use the coordinate table and examples when interpreting Maker/Tiled assets. Isometric maps and rotated map/camera transforms require their own mapping.
Physics Checklist
SCE 场景静态碰撞查询
客户端对当前 SCE 3D 场景进行本地物理查询前,如果需要命中地形、墙壁、平台等静态场景几何,应显式调用 Game.LocalScene.EnsureStaticMeshColliders()(或对当前 Scene 实例调用同名 API)加载静态碰撞信息,再执行 PhysicsWorld.Raycast、SphereCast 等查询。例如技能预瞄准、抛射轨迹和落点检测如果发现射线穿透墙壁,首先确认已在场景加载完成后执行该 API,并检查返回的 SceneColliderLoadResult;不要把这类查询依赖在单位控制器或其他自动物理流程上。
For dynamic local physics, let RigidBody own continuous motion:
csharp
body.LinearVelocity = new Vector3(input.X, input.Y, 0f) * speed;
body.WakeUp();Avoid writing node.Position every frame for the same dynamic body. Physics writes the body transform back to the node during simulation; mixing direct node movement and dynamic body simulation causes jitter or overwritten positions.
Use direct node transform only for visual-only objects or teleport/reset flows. For resets, clear velocities and wake the body.
Register OnCollisionBegin / OnCollisionBegin2D before the node's rigid body and collision shape can enter an active physics step. Begin events are edge events and are not replayed for contacts that started before subscription. If initial overlap matters, subscribe first and then run an explicit overlap query (OverlapSphere, OverlapBox, or the relevant 2D query).
GameSystemUI Input
For non-Unit targets, use MoveControl input-event mode:
csharp
var root = GSUI.GameDataControl.MoveControl.Data!.CreateControl();
root.AddToVisualTree();
var move = root.GetMoveControl();
if (move != null)
{
move.UseMainUnit = false;
move.BindUnit = null;
move.OnMoveInput += OnMoveInput;
}Choose the direction contract for the actual camera and movement plane:
Directionis a direction vector with raw screen axes, X right and Y down: W/joystick up is negative Y; zero means centered/stopped. It is not a top-left-origin pixel position.WorldDirectionrotates it by the default camera offset.UseMainUnit=false/BindUnit=nullprevent Unit orders but do not disable this calculation.- For camera-relative SCE world-plane movement, use
WorldDirectionand map the axes as needed. - For a fixed orthographic camera looking along Z at an XY map with screen right = +X and screen up = +Y, use
new Vector2(e.Direction.X, -e.Direction.Y). Do not apply this flip to already-converted TMX object coordinates. - The default offset first uses the main viewport camera's forward XY projection, then falls back to the SCE gameplay camera's
Yaw + 90 degrees. A camera looking along Z has no forward XY heading, so that fallback is not a reliable screen-to-world mapping for this 2D view. For rotated 2D cameras, derive the mapping from the actual camera's screen axes.
Unsubscribe and remove the control during dispose. Full semantics and the TmxMapSample example are in GameSystemUI movement controls.
Material Guidance
Prefer known SDK materials:
MaterialPBRfor the most reliable visible baseline.MaterialFlatLambertfor no-texture lit solid color debug or generated geometry when the SDK/runtime includes it.MaterialWaterfor water.MaterialCustom+Technique.Find(...)only when a strong type does not model the technique.
Do not assume a local XML technique is safe for external users until the engine resource and shader cache path are published. For invisible materials, use gamegraph-materials and Runtime MCP scenegraph.materials / material.probe.
Verification
Build both sides even for client-only gameplay so shared GameData and server stubs stay valid:
powershell
dotnet build src\GameEntry.csproj -c Client-Debug
dotnet build src\GameEntry.csproj -c Server-DebugFor a visible preview loop:
- Build and deploy the latest DLLs if launching client-only manually.
- Start client-only or editor debug.
- Call Runtime MCP
debug.ping. - Capture
debug.capture_screenshot. - Use
ui.snapshotfor GameUI andscenegraph.materials/material.probefor 3D material state. - For a game, inspect the normal playable view and a close-up, then a movement/attack sequence and failure/restart. Check framing, player/enemy separation, visible projectile travel, lighting transitions, HUD readability and input gating after game over. A material probe confirms state, not the final rendered appearance. Follow sce-preview-verify for the runtime evidence workflow.
Related
docs/best-practices/ClientSceneGraphGamePitfalls.mddocs/systems/GameGraphOverview.mddocs/systems/GameGraph2DAnimation.mddocs/systems/GameSystemUIMoveControl.mddocs/ai/skills/gamegraph-materials/SKILL.mddocs/ai/skills/client-only-debug/SKILL.mddocs/ai/skills/sce-preview-verify/SKILL.md