Unity6 Game Development · Lesson 26

Quaternion turning and local forward axes

Turn toward a local orientation without interpolating Euler components.

Unity 6 Editor walkthrough

The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.

A local goal and a world-facing axis describe different things

Imagine an arrow mounted on a rotating platform. The arrow’s local rotation describes its orientation relative to that platform, while its forward direction in the world changes with both rotations. LocalTurnProbe deliberately chooses a goal in the parent’s coordinates, allowing the parent experiment to reveal that distinction. Quaternion values represent rotations rather than editable degrees about independent axes. Building a goal with Quaternion.Euler is a convenience for configuration; the subsequent movement remains a quaternion operation. This lesson extends the earlier position-space exercise by following an orientation through a hierarchy and giving its angular motion an explicit budget.

Cache the goal and assign each returned orientation

Start converts the configured targetLocalEuler into goal once. Update multiplies a nonnegative degrees-per-second setting by Time.deltaTime, then passes the current localRotation, goal and maximum angular step to RotateTowards. Assigning the return value is essential; calling a static calculation alone would not rotate the object. DrawRay uses transform.forward, which is already a world direction, so there is no second TransformDirection conversion. The goal’s once-per-start lifetime also matters: changing the Inspector field during Play does not automatically rebuild the cached quaternion. A live retargeting feature would need an explicit method or a deliberate recalculation policy.

    private void Start() { goal = Quaternion.Euler(targetLocalEuler); }
    private void Update()
    {
        if (degreesPerSecond < 0f || float.IsNaN(degreesPerSecond) || float.IsInfinity(degreesPerSecond)) return;
        transform.localRotation = Quaternion.RotateTowards(transform.localRotation, goal, degreesPerSecond * Time.deltaTime);
        Debug.DrawRay(transform.position, transform.forward * 2f, Color.blue);
    }
}

Check a circular budget without inventing a quaternion engine

The scalar companion normalizes angles into a single-turn range and computes a signed shortest difference. From 350 to 10 the remaining turn is positive twenty degrees, so the first five-degree budget produces 355. Another five crosses the representation boundary and normalizes 360 to zero. A larger final budget clamps the difference and lands on ten instead of overshooting. This illustrates representation wraparound and bounded progress in one dimension. It does not establish quaternion multiplication order, gimbal behavior, rotation about arbitrary axes or a three-dimensional interpolation path. Those claims require the actual orientation API and a real transform hierarchy.

Avoid competing writers and misleading Euler comparisons

A model’s visible shape is not always aligned with its local positive Z axis, so an elongated imported mesh can appear to face sideways even while transform.forward is correct. Use a simple cube or arrow with a known orientation first. Keep an Animator and other rotation scripts from writing the same Transform during the baseline. Parent scale and import transforms can further complicate a visual diagnosis. Do not compare Euler displays component by component to decide whether two quaternion orientations match: equivalent orientations can have different angle representations. Choose an angular tolerance or a clear forward-axis observation instead of treating Inspector text as the mathematical identity of a rotation.

Practice: rotate the mounting platform, then retarget deliberately

Run the child under a zero-rotation parent and record its final local orientation and blue forward ray. Stop, rotate the parent by thirty degrees around Y and repeat without changing the child’s configured local goal. Predict what stays local and what changes in world space. As a second stage, add a public retarget method that computes a new goal from supplied Euler configuration, and invoke it only in Play mode. Keep that method separate from the per-frame turn. The exercise should expose the lifetime of a target command rather than silently rereading every Inspector field and obscuring when an orientation decision was made.

  • Hint: localRotation is relative to the parent; forward is expressed in world coordinates.
  • Hint: choose either local or world goals for an API and make that choice visible in its name.

Practice: document the scalar tie and invalid-input policies

Test a starting angle of ten and a goal of 350, expecting a negative shortest move. Then test equal angles, a zero budget and a budget larger than the remaining turn. The current scalar formula chooses one signed direction at an exact half turn; write that tie choice down before claiming it matches another rotation system. Extend input validation to the source angles if the helper becomes a reusable public API, because checking only the budget leaves nonfinite angle inputs outside its stated assumptions. These are independent arithmetic contracts, not evidence that the Unity component was imported or that a mesh’s forward axis was authored correctly.

  • Hint: compare angular values after normalization, particularly around zero.
  • Hint: no scalar helper can prove three-dimensional quaternion behavior it does not represent.

Review movement units and the chosen coordinate space

A correct rotation review can answer four concrete questions: where the target orientation lives, when it is captured, which unit controls movement and which object owns the final Transform write. The example answers local space, Start, degrees per second and this sole component. The fixed Blue ray is an observation aid rather than a gameplay dependency. Unity’s pinned API documents the nonovershooting angular step; the local validation only parsed the engine source as C#9 and executed the separate scalar lab. Preserve that distinction when moving to aiming, turrets or a physically driven body whose rotation must be controlled through another subsystem.

Runs in Unity 6 Editor

Unity Editor example

Set up in Unity 6

  1. Create an empty Parent with a child elongated Cube. Leave both rotations at zero and keep the parent scale uniformly one for a clear baseline.
  2. Attach LocalTurnProbe.cs to the child. Set targetLocalEuler to (0, 90, 0) and degreesPerSecond to 45.
  3. Enter Play mode and enable Gizmos in Scene view. Observe the child turn toward the configured local goal and the blue world-space forward ray.
  4. Stop Play mode, rotate Parent around Y by 30 degrees, then repeat. Compare local Rotation in the Inspector with the world-facing forward ray.
  5. To change the goal field, stop and restart this baseline; Start caches it once. Do not let an Animator or another rotation script simultaneously write the same Transform.

LocalTurnProbe.cs

Download Unity script ↗
using UnityEngine;
public sealed class LocalTurnProbe : MonoBehaviour
{
    [SerializeField] private Vector3 targetLocalEuler = new Vector3(0f, 90f, 0f);
    [SerializeField, Min(0f)] private float degreesPerSecond = 45f;
    private Quaternion goal;
    private void Start() { goal = Quaternion.Euler(targetLocalEuler); }
    private void Update()
    {
        if (degreesPerSecond < 0f || float.IsNaN(degreesPerSecond) || float.IsInfinity(degreesPerSecond)) return;
        transform.localRotation = Quaternion.RotateTowards(transform.localRotation, goal, degreesPerSecond * Time.deltaTime);
        Debug.DrawRay(transform.position, transform.forward * 2f, Color.blue);
    }
}

What to observe in Unity

The child approaches the cached local goal without an angular step overshooting it. A rotated parent changes the child’s world orientation while the local target remains the same.

The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;

var angle = 350.0;
angle = Step(angle, 10, 5); Print(angle);
angle = Step(angle, 10, 5); Print(angle);
angle = Step(angle, 10, 50); Print(angle);
double Normalize(double x) => ((x % 360) + 360) % 360;
double Step(double from, double to, double maximum)
{
    if (maximum < 0 || !double.IsFinite(maximum)) throw new ArgumentOutOfRangeException(nameof(maximum));
    var difference = Normalize(to - from + 180) - 180;
    return Normalize(from + Math.Clamp(difference, -maximum, maximum));
}
void Print(double x) => Console.WriteLine(x.ToString("0", CultureInfo.InvariantCulture));
Open C# practice

C# practice output

355
0
10
Try this
Choose the coordinate space before choosing the quaternion operation.

Further reading