Skip to main content

entities-and-factories

Entities and Factories in FlatRedBall2. Use when working with Entity subclasses, generic Factory types, spawning/creating/destroying entities, entity lifecycle, Add, shape children, CustomInitialize/CustomActivity, or Engine.GetFactory. Trigger on any entity creation, destruction, or factory question.

跳到安装

来源信息

仓库
vchelaru/FlatRedBall2
最近来源活动
2026年9月3日 15:00
检测到的 SKILL.md 语言
英语
星标
14
分支
6

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
3 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
entities-and-factories
description
Entities and Factories in FlatRedBall2. Use when working with Entity subclasses, generic Factory types, spawning/creating/destroying entities, entity lifecycle, Add, shape children, CustomInitialize/CustomActivity, or Engine.GetFactory. Trigger on any entity creation, destruction, or factory question.
# Entities and Factories in FlatRedBall2 `Entity` is the base class for game objects. It owns position, velocity, acceleration, drag, and a list of child shapes for collision and rendering. `Factory<T>` manages creating, tracking, and destroying entity instances from within a `Screen`. ## Rules 1. **Always spawn through `Factory<T>`** — never `new MyEntity()`. Bypassing the factory breaks `Engine.GetFactory<T>()` and collision relationships. This applies even when there is only one instance (e.g., one ball in Pong). 2. **Override `CustomInitialize` for setup, `CustomActivity` for per-frame logic.** Add shape children, create input handlers, and wire references in `CustomInitialize`. The constructor is too early — `Engine` is null until the factory injects it (see `engine-overview`). 3. **Don't write properties whose only effect happens in `CustomInitialize`.** They look configurable but silently fail when assigned after `Create()` returns. Three fixes by case: expose the child shape directly (forwarding), pass init-only data through `Create(e => e.X = ...)` so it's set before `CustomInitialize` runs, or write a reactive setter for state the gameplay legitimately mutates. See `references/reactive-properties.md` — this is the most common entity-design footgun in FRB2. 4. **Don't create entities for static walls / floors / ceilings.** Use `TileShapes` instead — see `collision-relationships`. 5. **Entity `(X,Y)` should be the object's ground-contact point, not its sprite's visual center.** `Sprite` always draws centered on its entity, so a sprite taller/wider than a point needs `Sprite.X`/`Sprite.Y` offset once its size is known. See `platformer-movement` (feet-at-origin) and `top-down-movement` (origin and draw order) for perspective-specific offsets. This code-only path is fine for a one-off entity. If the entity already has (or will get) an `.achx`, author the sprite offset and its collision shape together as frame data instead — see `animation` skill's per-frame shapes — so the offset lives in content, not `CustomInitialize` math. ## Lifecycle Order 1. `Factory<T>.Create()` — allocates the entity, sets `Engine`, calls `AddEntity` on the screen 2. `CustomInitialize()` — called immediately after; add shape children and initialize input here 3. Each frame: physics update → collision resolution → `CustomActivity(time)` ## Minimal Entity Example ```csharp public class Player : Entity { private KeyboardInput2D _movement = null!; public AARect Rectangle { get; private set; } = null!; public override void CustomInitialize() { Rectangle = new AARect { Width = 40, Height = 40, Color = new Color(80, 140, 255, 220), IsVisible = true, }; Add(Rectangle); _movement = new KeyboardInput2D( Engine.Input.Keyboard, Keys.Left, Keys.Right, Keys.Up, Keys.Down); } public override void CustomActivity(FrameTime time) { const float Speed = 200f; VelocityX = _movement.X * Speed; VelocityY = _movement.Y * Speed; } } ``` `Rectangle` is exposed directly as a public auto-property so callers can write `player.Rectangle.Color = ...` at any time. Do not wrap it in a forwarding property like `Color` or `FillColor` — see `references/reactive-properties.md` for why. For shape types and visual properties (`IsVisible`, `Color`, `IsFilled`, etc.), see the `shapes` skill. Shapes default to `IsVisible = false` — always set it explicitly. ## Using `Factory<T>` from a Screen ```csharp public class GameScreen : Screen { private Factory<Player> _playerFactory = null!; public override void CustomInitialize() { _playerFactory = new Factory<Player>(this); var player = _playerFactory.Create(); player.X = 100; player.Y = 50; } } ``` `Factory<T>` implements `IEnumerable<T>` — pass it directly to `AddCollisionRelationship`. `Create(Action<T>)` runs the callback after engine injection but before `CustomInitialize`, so init-only fields are guaranteed-set when the entity reads them: `_asteroidFactory.Create(a => a.Size = AsteroidSize.Small)`. Use this instead of "create, then assign" whenever the value is consumed inside `CustomInitialize`. See `references/reactive-properties.md`. `Factory<T>.Instances` exposes the live list as `IReadOnlyList<T>`: ```csharp if (_brickFactory.Instances.Count == 0) MoveToScreen<NextLevelScreen>(); ``` `Engine.GetFactory<T>()` looks up a factory by type — used when spawning from inside another entity. Throws if no factory for `T` exists yet on the screen. ## Destroying Entities ```csharp enemy.Destroy(); // removes from factory, screen, and clears child shapes ``` `factory.Destroy(entity)` is equivalent. **Fields are invalid after `Destroy()`** — don't read state on an entity you just destroyed; use `factory.Instances.Count == 0` to detect when all are gone. ## Object Pooling for High-Churn Entities Bullets, particles, score popups — entities that spawn and die many times per second — generate avoidable GC pressure. Opt the factory into pooling: ```csharp _bulletFactory = new Factory<Bullet>(this).EnablePooling().Prewarm(32); ``` With pooling on, `Destroy()` returns the instance to a free list instead of tearing it down; the next `Create()` reuses it. `EnablePooling()` must be called before the factory has produced any live instance — throws otherwise. **Contract:** - `CustomInitialize` runs exactly once per instance, on first `Create()`. Shape children allocated there are reused across every recycle — the whole point. - `CustomDestroy` does **not** run when a pooled entity is destroyed. Use it only for one-time teardown of resources allocated in `CustomInitialize`. - The engine resets per-life state automatically on recycle: `Position`, `Velocity`, `Acceleration`, `Rotation`, `RotationVelocity`, `Drag`, `Z`, `IsVisible`. - Override `protected void Reset()` to clear **entity-specific dynamic state** the entity itself mutates over its life — lifetime accumulators, health, mode flags, internal state-machine cursors. Forgetting to reset these is the pooling footgun: stale state bleeds into the next life and is hard to diagnose. Skip pooling for entities that exist as singletons or near-singletons (player, level boss, HUD-anchored UI). The opt-in API exists so the default path stays predictable. ## Fire-and-Forget Effects For short-lived visual entities the spawner doesn't want to keep a reference to — explosions, hit sparks, dust puffs, falling enemy bodies, damage numbers — skip the subclass and factory entirely. `Screen.CreateFireAndForget` builds and registers a one-shot `Entity` with a `Sprite` child and self-destroys when the animation finishes (or after a duration for the texture overload). ```csharp // Plays once and destroys on AnimationFinished — IsLooping is forced to false var fx = CreateFireAndForget(_explosionAchx, "Explode", x, y); // Static texture for `duration` seconds, then destroys var num = CreateFireAndForget(_damageTex, x, y, duration: 0.5f); num.VelocityY = 60f; ``` The returned `Entity` is fully wired — set `Velocity`/`Acceleration`, `AttachTo` a parent, or `Add` shapes for collision before the next frame. Use a real `Entity` subclass + `Factory<T>` instead when the effect needs gameplay logic, queryable state, or a looping animation with timed cleanup. ## Entity.Name Optional `string?` for identifying entities in tests and diagnostics. `SceneSnapshot.Named("player")` matches case-insensitively. Has no effect on collision, rendering, or lifecycle. ## See Also - `references/reactive-properties.md` — property-vs-child-shape decision; the most common entity-design footgun - `references/patterns.md` — render-only shapes (`isDefaultCollision`), solid-grid factories (`IsSolidGrid`), spawning from within an entity, death effects, particles, configuring after `Create()` - `shapes` skill — shape types, visibility, color, render pipeline registration - `collision-relationships` skill — `AddCollisionRelationship` over a `Factory<T>`, `TileShapes` - `levels` skill — `TileMap.CreateEntities` for designer-placed entities ## Common Pitfalls - **Naming fields the same as `Entity` members.** `Acceleration`, `Velocity`, `Drag` already exist on `Entity` — shadowing them causes warnings. - **Initializing input objects every frame.** Create `KeyboardInput2D` and similar in `CustomInitialize`, not `CustomActivity`. - **`Add(child)` before `Engine` is set.** Auto-registration to the render pipeline only happens once `Engine` is set; Factory sets it before `CustomInitialize`, so `Add` works correctly there.
在 GitHub 查看