Fixed step and every frame
The simulation runs at a fixed rate; rendering runs as fast as it can. Decide and move in OnFixedUpdate on the exact pose; drive visuals in OnUpdate from the interpolated pose.
Two clocks
Character controllers, rigid bodies, navigation agents and OnFixedUpdate move entities only in fixed steps (60 per second by default). Rendering, OnUpdate and OnLateUpdate run once per frame, at any frame rate. At 240 frames per second a 60 Hz character moves on one frame in four.
| You want to | Run it in | Read |
|---|---|---|
| Make gameplay decisions, move things, apply forces, read input for movement | OnFixedUpdate | Transform.WorldPosition and WorldRotation (exact), CharacterController.GetAchievedVelocity, IsGrounded |
| Drive visuals from motion: animation parameters, markers, camera smoothing, audio pitch from speed | OnUpdate / OnLateUpdate | Transform.InterpolatedPosition and InterpolatedRotation, Time.FixedAlpha |
| Teleport or place an entity | either | write Transform.WorldPosition (writes are always exact) |
Why it matters
Transform.WorldPosition is the exact simulation pose. Read every frame, it repeats on the frames between fixed steps and then jumps: a speed measured per frame from it reads zero, zero, zero, then four times too fast. Transform.InterpolatedPosition is the pose the renderer draws this frame, between the last two fixed steps at Time.FixedAlpha. It lags the simulation by up to one step and moves smoothly at any frame rate.
CharacterPresentation.cs
public sealed class CharacterPresentation : EntityScript { public string MarkerName = "Marker"; // a floating marker, a child of the level root private Entity marker; protected override void OnStart() => marker = Entity.FindChild(MarkerName, recursive: true); protected override void OnFixedUpdate(float fixedDeltaTime) { // Decisions and motion: the exact simulation pose. if (Transform.WorldPosition.Y < -50.0f) { Transform transform = Transform; transform.WorldPosition = new Vector3(0.0f, 2.0f, 0.0f); // writes are always exact } } protected override void OnUpdate(float deltaTime) { // Visuals: the pose the renderer draws this frame, smooth at any frame rate. if (marker.IsAlive) { Transform markerTransform = marker.Transform; markerTransform.WorldPosition = Transform.InterpolatedPosition + Vector3.UnitY * 2.2f; } Vector3 velocity = CharacterController.GetAchievedVelocity(Entity); Animator.SetFloat(Entity, "Speed", new Vector2(velocity.X, velocity.Z).Length()); } }
Characters
CharacterController.GetVelocity is the motion that was asked for; GetAchievedVelocity is what the last fixed-step move achieved after collisions, slides and steps: zero against a wall. It stays constant between fixed steps, so it is smooth to read every frame and right for animation speed.
CharacterController.IsGrounded rides out ground-contact losses shorter than the Grounded Grace (0.1 s by default) that seams, curbs and bumps cause. HasGroundContact is the raw contact of the last step, for effects that must be exact.
Input in fixed steps
Input actions read in OnFixedUpdate report each press and release on exactly one fixed step, so a jump is never lost at a high frame rate and never doubled at a low one. In OnUpdate, they follow the rendered frame.
On the dedicated server there is no rendering: InterpolatedPosition equals WorldPosition and Time.FixedAlpha is 1.
KroysEngine 0.1.0 documentation, built from the engine repository by Tools/BuildDocsSite.py.