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
- Always spawn through
Factory<T>— nevernew MyEntity(). Bypassing the factory breaksEngine.GetFactory<T>()and collision relationships. This applies even when there is only one instance (e.g., one ball in Pong). - Override
CustomInitializefor setup,CustomActivityfor per-frame logic. Add shape children, create input handlers, and wire references inCustomInitialize. The constructor is too early —Engineis null until the factory injects it (seeengine-overview). - Don't write properties whose only effect happens in
CustomInitialize. They look configurable but silently fail when assigned afterCreate()returns. Three fixes by case: expose the child shape directly (forwarding), pass init-only data throughCreate(e => e.X = ...)so it's set beforeCustomInitializeruns, or write a reactive setter for state the gameplay legitimately mutates. Seereferences/reactive-properties.md— this is the most common entity-design footgun in FRB2. - Don't create entities for static walls / floors / ceilings. Use
TileShapesinstead — seecollision-relationships. - Entity
(X,Y)should be the object's ground-contact point, not its sprite's visual center.Spritealways draws centered on its entity, so a sprite taller/wider than a point needsSprite.X/Sprite.Yoffset once its size is known. Seeplatformer-movement(feet-at-origin) andtop-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 — seeanimationskill's per-frame shapes — so the offset lives in content, notCustomInitializemath.
Lifecycle Order
Factory<T>.Create()— allocates the entity, setsEngine, callsAddEntityon the screenCustomInitialize()— called immediately after; add shape children and initialize input here- Each frame: physics update → collision resolution →
CustomActivity(time)
Minimal Entity Example
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
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>:
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
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:
_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:
CustomInitializeruns exactly once per instance, on firstCreate(). Shape children allocated there are reused across every recycle — the whole point.CustomDestroydoes not run when a pooled entity is destroyed. Use it only for one-time teardown of resources allocated inCustomInitialize.- 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).
// 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 footgunreferences/patterns.md— render-only shapes (isDefaultCollision), solid-grid factories (IsSolidGrid), spawning from within an entity, death effects, particles, configuring afterCreate()shapesskill — shape types, visibility, color, render pipeline registrationcollision-relationshipsskill —AddCollisionRelationshipover aFactory<T>,TileShapeslevelsskill —TileMap.CreateEntitiesfor designer-placed entities
Common Pitfalls
- Naming fields the same as
Entitymembers.Acceleration,Velocity,Dragalready exist onEntity— shadowing them causes warnings. - Initializing input objects every frame. Create
KeyboardInput2Dand similar inCustomInitialize, notCustomActivity. Add(child)beforeEngineis set. Auto-registration to the render pipeline only happens onceEngineis set; Factory sets it beforeCustomInitialize, soAddworks correctly there.