Rotation Code Optimization – Manual vs DOTween

This type of rotation animation is used across multiple objects in the game—from player turns to environmental elements and UI transitions. Because it runs frequently and across many instances, optimizing it early was critical for reducing CPU overhead, ensuring consistent frame times, and keeping GC allocations low—especially on mobile.

Before

Quaternion startRotation = player.transform.rotation;
Quaternion endRotation = Quaternion.Euler(0, targetYAngle, 0);
float elapsed = 0f;

while (elapsed < duration)
{
    elapsed += Time.deltaTime;
    player.transform.rotation = Quaternion.Slerp(startRotation, endRotation, elapsed / duration);
    yield return null;
}

After (with DOTween)

// we use DOTween for interpolation and easing
player.transform.DORotate(new Vector3(0, targetYAngle, 0), duration).SetEase(Ease.OutSine);
yield return player.transform
    .DORotate(new Vector3(0, targetYAngle, 0), 0)
    .WaitForCompletion();
Why this is better:
  • Reduced lines of code: From 9 lines to 2—cleaner and easier to maintain.
  • Improved performance: DOTween handles interpolation internally using optimized C# coroutines, avoiding extra GC pressure and CPU usage from per-frame math.
  • Smoother animation: Built-in easing like Ease.OutSine makes transitions feel natural without manual math.
  • GC friendly: No per-frame Quaternion recalculation or struct allocations—DOTween uses pooling and caching behind the scenes.

Trajectory Preview Optimization — Manual Loop vs Burst Job System

This trajectory preview runs every frame while the shot is charging and for multiple objects. Because it’s high-frequency and touches lots of math per frame, pushing it off the main thread with Burst was a priority to reduce CPU spikes and keep frame times stable on mobile.

Before

private void DrawTrajectory(float force)
{
    frameCounter++;
    Vector3 start = ball.position;
    Vector3 targetPos = Target.position;
    Vector3 toTarget = (targetPos - start);
    float toTargetLen = toTarget.magnitude;
    Vector3 toTargetN = (toTargetLen > 1e-5f) ? (toTarget / toTargetLen) : Vector3.forward;
    float curveVal = (curveSlider != null) ? curveSlider.value : 0f;

    bool inputsChanged =
        cachedCharging != isCharging ||
        Mathf.Abs(cachedForce - force) > 0.001f ||
        Mathf.Abs(cachedCurveVal - curveVal) > 0.001f ||
        (cachedStart - start).sqrMagnitude > 1e-4f ||
        (cachedTargetPos - targetPos).sqrMagnitude > 1e-4f;

    if (!inputsChanged && isCharging && (frameCounter % recomputeEveryNFrames != 0))
    {
        trajectoryLine.startColor = startColorCached;
        trajectoryLine.endColor = endColorCached;
        return;
    }

    cachedCharging = isCharging;
    cachedForce = force;
    cachedCurveVal = curveVal;
    cachedStart = start;
    cachedTargetPos = targetPos;
    cachedToTarget = toTargetN;

    if (!isCharging)
    {
        trajCount = 2;
        trajPoints[0] = start;
        trajPoints[1] = targetPos;

        trajectoryLine.positionCount = trajCount;
        trajectoryLine.SetPosition(0, trajPoints[0]);
        trajectoryLine.SetPosition(1, trajPoints[1]);

        trajectoryLine.startColor = startColorCached;
        trajectoryLine.endColor = endColorCached;
        return;
    }

    Vector3 sideDir = Vector3.Cross(Vector3.up, toTargetN).normalized;
    Vector3 lateralAccel = sideDir * (curveVal * curveFactor);

    trajCount = 0;
    Vector3 pos = start;
    Vector3 vel = toTargetN * force;
    float traveled = 0f;

    trajPoints[trajCount++] = pos;

    float dt = trajectoryTimeStep;
    Vector3 acc = Physics.gravity + lateralAccel;
    float maxDist = maxRenderDistance;
    int maxPts = Mathf.Min(maxTrajectoryPoints, trajPoints.Length - 1);

    for (int i = 0; i < maxPts; i++)
    {
        Vector3 nextVel = vel + acc * dt;
        Vector3 nextPos = pos + nextVel * dt;

        Vector3 seg = nextPos - pos;
        float segLen = seg.magnitude;

        if (traveled + segLen > maxDist)
        {
            float remain = maxDist - traveled;
            Vector3 dir = segLen > 1e-6f ? (seg / segLen) : Vector3.forward;
            nextPos = pos + dir * remain;
            trajPoints[trajCount++] = nextPos;
            break;
        }

        int hits = 0;
        float castDist = Mathf.Max(0f, vel.magnitude * dt);
        if (castDist > 1e-6f)
            hits = Physics.RaycastNonAlloc(pos,
			vel.normalized, ray1, castDist, ~0, QueryTriggerInteraction.Ignore);

        if (hits > 0)
        {
            var hit = ray1[0];
            trajPoints[trajCount++] = hit.point;

            vel = Vector3.Reflect(vel, hit.normal) * bounceFactor;
            pos = hit.point + vel.normalized * 0.01f;

            Vector3 prev = trajPoints[trajCount - 2];
            traveled += Vector3.Distance(prev, hit.point);
            continue;
        }

        vel = nextVel;
        pos = nextPos;
        traveled += segLen;

        trajPoints[trajCount++] = pos;
    }

    trajectoryLine.positionCount = trajCount;
    for (int i = 0; i < trajCount; i++)
        trajectoryLine.SetPosition(i, trajPoints[i]);

    trajectoryLine.startColor = startColorCached;
    trajectoryLine.endColor = endColorCached;
}

After (Burst + Job System)

private void DrawTrajectory(float force)
{
    Vector3 start = ball.position;
    Vector3 toTarget = (Target.position - start);
    float len = toTarget.magnitude;
    Vector3 dirN = (len > 1e-5f) ? (toTarget / len) : Vector3.forward;

    if (!isCharging)
    {
        trajectoryLine.positionCount = 2;
        trajectoryLine.SetPosition(0, start);
        trajectoryLine.SetPosition(1, Target.position);
        trajectoryLine.startColor = 
				new Color(lineBaseColor.r, lineBaseColor.g,
								lineBaseColor.b, 1f);
        trajectoryLine.endColor   =
				new Color(lineBaseColor.r, lineBaseColor.g, 
								lineBaseColor.b, tailAlpha);
        return;
    }

    float curveVal = curveSlider != null ? curveSlider.value : 0f;
    Vector3 sideDir = Vector3.Cross(Vector3.up, dirN).normalized;
    Vector3 lateralAccel = sideDir * (curveVal * curveFactor);
    Vector3 accel = Physics.gravity + lateralAccel;
    Vector3 initialVel = dirN * Mathf.Max(0f, force);
   
    int count = burstPreview.Predict(
        start: start,
        initialVelocity: initialVel,
        accel: accel,
        dt: trajectoryTimeStep,
        maxPoints: maxTrajectoryPoints,
        maxDistance: maxRenderDistance
    );

    int finalCount = count;
    for (int i = 0; i < count - 1; i++)
    {
        Vector3 p0 = burstPreview.GetPoint(i);
        Vector3 p1 = burstPreview.GetPoint(i + 1);
        Vector3 seg = p1 - p0;
        float segLen = seg.magnitude;
        if (segLen <= 1e-6f) continue;

        if (Physics.Raycast(p0, seg / segLen, out var hit, segLen))
        {
            trajBuffer[0] = burstPreview.GetPoint(0);
            for (int j = 1; j <= i; j++) trajBuffer[j] = 
								burstPreview.GetPoint(j);
            trajBuffer[i + 1] = hit.point;
            finalCount = i + 2;
            goto PUSH;
        }
    }
  
    for (int i = 0; i < count; i++)
        trajBuffer[i] = burstPreview.GetPoint(i);

PUSH:
    trajectoryLine.positionCount = finalCount;
    for (int i = 0; i < finalCount; i++)
        trajectoryLine.SetPosition(i, trajBuffer[i]);

    trajectoryLine.startColor =
		new Color(lineBaseColor.r, lineBaseColor.g, lineBaseColor.b, 1f);
    trajectoryLine.endColor   =
		new Color(lineBaseColor.r, lineBaseColor.g, lineBaseColor.b, tailAlpha);
}
Why this is better (Burst + Jobs):
  • Main-thread time down: The O(N) integration loop moves to a Burst-compiled job, cutting per-frame CPU on the main thread during charge previews.
  • SIMD & cache-friendly math: Burst vectorizes arithmetic and tight loops, often unrolling and using fast-math where safe—meaning the same math costs fewer cycles.
  • No per-frame GC churn: Positions are written to a pre-allocated buffer (e.g., NativeArray<float3>), then copied to a persistent trajBuffer. No ToArray() or transient allocations.
  • Scales gracefully: Need more preview points or a smaller dt for smoother curves? The heavy work stays off the main thread, so FPS remains stable.
  • Deterministic pathing (optional): Fixed dt + pure math in the job yields consistent results across devices (within float tolerance).
  • Collision kept cheap: A single ray test per segment on the main thread to cut at first impact keeps physics interactions simple and affordable.

Result: Main Player loop Now have 0 GC collection as compared to having 40Bytes per every time we draw trajectory, eliminating charge-time hitches. Which will help when multiple players would be using this Trajectory.

Implementation : I am Marking the job with [BurstCompile], using Unity.Mathematics for math, storing points in a persistent NativeArray<float3> (Allocator.Persistent), Schedule() the job, Complete() before reading, reuse buffers each frame, and Dispose() in OnDestroy().

Updated Graphics

Before – early art style
After – updated art style

I updated the art direction by replacing early placeholder assets with low‑poly models, giving the game a cleaner, more distinct visual style that enhances its looks.

Screenshots

Contact

Got any Suggestions or queries? Reach out or check out the game on Play Store.