Engine · guide
Players and possession
A controller that outlives the body it drives, and the one component that carries a player's intent.
Edit this page on GitHubDocuments
What it is#
A player is an entity carrying a PlayerController: a seat at the machine, an aim, and nothing
that describes a thing in the world. A pawn is any entity it is currently driving. Player is
the operation set that links the two, and MoveIntent is what the link carries.
The three pieces are separate because they have different lifetimes. A pawn dies. A controller does not — it keeps its slot, its connection, its camera channel and, most importantly, where the player was looking.
What it is for#
Anything a person drives: a character, a vehicle, a spectator camera, a cursor in a strategy game.
The reason to use it rather than a script on the character is respawning. With a controller, a
respawn is Player.Possess(world, controller, newPawn) — one call, and the aim, the score and the
camera follow by themselves. Without one, every game rediscovers that those five things have to be
copied across the gap, and each rediscovery misses a different one.
It is also what an AI shares with a human. Nothing downstream of MoveIntent knows which is driving,
so a possessed NPC and a possessed player run the same movement code.
You do not want it for something nobody drives — a door, a turret on a timer, a projectile. Those write their own components, and a pawn nothing possesses is never visited.
Using it#
Three components and one call:
using Vixen.Core;using Vixen.Ecs;using Vixen.Engine.Players;using Vixen.Engine.Transforms;public static class Spawning { public static Entity NewPlayer(World world) { var pawn = Hierarchy.CreateTransform(world, LocalTransform.Identity); var controller = Player.Create(world); Player.Possess(world, controller, pawn); return controller; }}Player.Create gives the entity a PlayerController, a ControlRotation and a MoveIntent.
Possess writes the Possessing/PossessedBy pair, releasing whatever either side was already
attached to — so possessing a pawn somebody else holds *steals* it rather than sharing it.
Two systems make it move. PlayerInputSystem samples each player's input source into their aim and
their intent; PossessionSystem copies that intent onto the pawn, points the camera at it, and lets
go of pawns that no longer exist. Both run in SystemPhase.Input, before the fixed step that reads
the result.
using Vixen.Engine.Frames;using Vixen.Engine.Players;public static class PlayerLoop { public static void Register(EngineLoop loop, PlayerInputSystem input) { loop.Add(input); loop.Add(new PossessionSystem()); }}Where intent comes from is an interface. IPlayerInputSource has one method, so a device, a
planner, a replay and a test are the same shape:
using Vixen.Engine.Players;public sealed class WalkForward : IPlayerInputSource { public void Sample(ref ControlRotation rotation, ref MoveIntent intent, float deltaTime) { rotation.Turn(0.5f * deltaTime, 0f); intent.Move = new(0f, 1f); intent.Yaw = rotation.Yaw; intent.Pitch = rotation.Pitch; }}ActionPlayerInput is the one the engine ships, over a Vixen.Input action map. It wants a Move
and a Look value action and binds any of Jump, Crouch, Sprint, Fire, AltFire, Aim,
Interact and Reload that the map happens to have. A missing Move or Look throws at
construction naming the map and the action, rather than reading zero on the frame the player presses
it.
⚠ MoveIntent.Move.Y is forward, and Vixen.Input's vector2 composite reports up as negative.
Those are two different conventions — one describes a direction of travel, the other a screen — and
ActionPlayerInput is the single place they meet, so it negates the Y it reads. A source of your own
owes the same flip: Move = (0, 1) has to mean forward, because WorldDirection multiplies it by a
forward vector. Copying the composite straight through walks the player backwards, and the symptom
reads as "W and S are swapped" rather than as a sign.
> A shipping game should implement IPlayerInputSource over its own generated accessor.
> ActionPlayerInput binds by name, which is the one thing Vixen.Input otherwise makes impossible:
> a renamed action becomes a run-time surprise instead of a compiler error. The engine cannot
> reference a game's generated class, so the default is the compromise; the interface is the way out
> of it, and it is the eight lines above.
The camera is one call. PlayerCameras assembles the two rigs a player steers:
using Vixen.Core;using Vixen.Ecs;using Vixen.Engine.Players;public static class Views { public static PlayerCamera Eyes(World world, Entity controller) => PlayerCameras.FirstPerson(world, controller, eyeHeight: 1.7f); public static PlayerCamera OverTheShoulder(World world, Entity controller) => PlayerCameras.ThirdPerson(world, controller, distance: 4f, shoulderHeight: 1.4f);}ThirdPerson also puts a CameraOcclusion on the shot, at the same pivot its body and aim stages
use. It does nothing until the host answers for it — what a camera may pass through is a question
about a world's solidity, and Vixen.Engine references no physics — so the one line a game with a
physics scene writes is:
loop.Add(new VirtualCameraSystem { Occlusion = new MyOcclusion(physics) });⚠ Set CameraOcclusion.PivotOffset to match the rig, which ThirdPerson does for you. The sweep
starts at whatever the shot looks at, and a character's origin is at their feet — so a probe of any
radius begins inside the floor, reports an obstruction at zero distance, and pins the camera at
MinimumDistance for ever. That reads as broken damping rather than as a ray starting in the ground.
Each creates a real Camera with a CameraDirector on the player's own channel, a VirtualCamera
shot on the same channel, and binds the shot. From then on PossessionSystem points the shot at
whatever the player is driving and feeds it their aim, every frame — so a death, a respawn and a
vehicle entry all need no camera code at all.
These two are in the engine rather than in a sample because they are the rigs that cannot be built
from outside it: both are steered by ControlRotation, and the write that carries it into PovAim
and OrbitBody is PossessionSystem's. Everything a game tunes is an argument here and a component
afterwards, so a third rig is the same three component adds with different values.
You can also do it by hand, which is what those two calls are:
using Vixen.Core;using Vixen.Ecs;using Vixen.Engine.Cameras;using Vixen.Engine.Players;public static class OwnRig { public static void Watch(World world, Entity controller, Entity pawn) { var shot = world.Create(VirtualCamera.Default, FollowBody.Behind(distance: 6f, height: 2f)); Player.BindCamera(world, controller, shot); Player.Possess(world, controller, pawn); }}⚠ FollowBody.Behind swings round as the target turns, which is right for a camera watching a car
and wrong for one a player is steering — that is why ThirdPerson uses OrbitBody instead.
Examples#
Respawning. The controller is what survives, so nothing has to be carried across:
using Vixen.Core;using Vixen.Ecs;using Vixen.Engine.Players;using Vixen.Engine.Transforms;public static class Deaths { public static Entity Respawn(World world, Entity controller, LocalTransform at) { var body = Hierarchy.CreateTransform(world, at); // The aim, the slot, the camera channel and the bound shot are all still there. Player.Possess(world, controller, body); return body; }}Destroying a pawn without unpossessing is fine. PossessionSystem clears the dangling edge on its
next pass and counts it in ReleasedCount — a number that climbing every frame means a game is
leaking possessions.
Split screen. Two seats, two gamepad slots, two camera channels, one world:
using Vixen.Core;using Vixen.Ecs;using Vixen.Engine.Players;public static class SplitScreen { public static (PlayerCamera One, PlayerCamera Two) TwoSeats(World world) { var one = Player.Create(world); var two = Player.Create(world, slot: 1); // The camera channel comes from the slot, so neither director can see the other's shots and // neither player can lose their camera to the other's trigger volume. return (PlayerCameras.ThirdPerson(world, one), PlayerCameras.ThirdPerson(world, two)); }}⚠ That simulates, and only seat zero is drawn. Each player gets their own director, shots and
camera, and all of it updates independently — but CameraExtractionSystem fills one RenderView from
the lowest Camera.Order in the world, and a RenderView has no viewport rectangle. PlayerCameras
sets each camera's order from its channel, so seat zero is on screen and swapping which player is
watched is an order write. Showing both at once needs a view per player and a rect on each, which is
the rendering pipeline's work rather than this subsystem's.
Reading the intent. Movement code sees a component, not a controller:
using Vixen.Ecs;using Vixen.Engine.Players;using Vixen.Engine.Transforms;public static class Walking { static readonly QueryDescription Walkers = new QueryDescription().WithAll<MoveIntent, LocalTransform>(); public static void Step(World world, float deltaTime) { foreach (var chunk in world.Chunks(Walkers)) { var intents = chunk.ReadValues<MoveIntent>(); var transforms = chunk.Values<LocalTransform>(); for (var index = 0; index < chunk.Count; index++) { var speed = intents[index].IsHeld(MoveButtons.Sprint) ? 8f : 4f; transforms[index].Position += intents[index].WorldDirection() * speed * deltaTime; } } }}WorldDirection uses the yaw and never the pitch, so a character walking forward while looking at
the sky walks along the ground.
Stopping input without losing the player. A cutscene sets one field:
using Vixen.Core;using Vixen.Ecs;using Vixen.Engine.Players;public static class Cutscenes { public static void Playing(World world, Entity controller, bool playing) => world.Get<PlayerController>(controller).AcceptsInput = !playing;}The intent is cleared rather than frozen, so a held sprint does not survive the cutscene — and the aim is untouched, because that is the thing the controller exists to preserve.
See also#
- Components — why
Possessing,PossessedByandViewTargetcarry -
[Component]without[DataContract], and what that keeps out of a scene file. - Entity queries — how a movement system reads
MoveIntenta column at a time. - Getting content into a running game — what a build has to know before a
- level can name the components a game like this declares.
The design record is docs/plan/29-players-and-possession.md, which carries the argument for the
decomposition and what P1 to P4 add: character movement, the assembled camera rigs, the networked and
predicted half, and the sample. The shots and the director a ViewTarget points into are
docs/plan/26-virtual-cameras.md.