top of page
Paddy Benson

Capstone Project: Week 8

Updated: May 18

During the break and first week back I was quite productive. The major thing I worked on was getting other racers onto the track for the player to race against. After many attempts trying to use different ai methods such as nav mesh and spline and way-point systems, I decided to look elsewhere. Thinking about all the racing games I had played I realised that a lot of them used some sort of ghost replay system where it records the players lap and shows happening again. I thought what if I took that and then just changed the mesh to be another racer, and added collisions. A surprisingly simple idea compared to ai logic. I found a tutorial and quickly had it implemented within my game.



The way it works is very simple and is built with only three scripts. The first is just to create the scriptable object itself and then allow for data clearing in case you want to reset it.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu]
public class Ghost : ScriptableObject
{
    public bool isRecord;
    public bool isReplay;
    public float recordFrequency; 

    public List<float> timeStamp;
    public List<Vector3> position;
    public List<Quaternion> rotation; // Changed to Quaternion for rotation data

    public void ResetData()
    {
        timeStamp.Clear();
        position.Clear();
        rotation.Clear();
    }    
}

The second is the Ghost Recorder script. This works by taking the position and rotation values and then storing them at what time they happen. It is also variable, allowing for the change at which values are recorded every second. For me the magic number is 50 updates per second (which I will explain why later).


using System.Collections;
using System.Collections.Generic;
using UnityEngine;



public class GhostRecorder : MonoBehaviour

{

    public Ghost ghost;

    private float timer;

    private float timeValue;



    private void Awake()

    {

        if (ghost.isRecord)

        {

            ghost.ResetData();

            timeValue = 0;

            timer = 0;

        }



    }



    void FixedUpdate()

    {

        timer += Time.unscaledDeltaTime;

        timeValue += Time.unscaledDeltaTime;



        if (ghost.isRecord & timer >= 1 / ghost.recordFrequency)

        {

            ghost.timeStamp.Add(timeValue);

            ghost.position.Add(this.transform.position);

            ghost.rotation.Add(this.transform.rotation);



            timer = 0;

        }

    }

                

}

And finally is the Ghost Player. All this does is then read all the data stored within the recorded data section of the scriptable object and replay it on play.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;



public class GhostPlayer : MonoBehaviour

{

    public Ghost ghost;

    private float timeValue;

    private int index1;

    private int index2;



    private void Awake()

    {

        timeValue = 0; 

    }



    void FixedUpdate()

    {

        timeValue += Time.unscaledDeltaTime;



        if (ghost.isReplay)

        {

            GetIndex();

            SetTransform();

        }

    }



    private void GetIndex()

    {

        for (int i = 0; i < ghost.timeStamp.Count - 2; i++)

        {

            if (ghost.timeStamp[i] == timeValue)

            {

                index1 = i;

                index2 = i;

                return;

            }

            else if (ghost.timeStamp[i] < timeValue & timeValue < ghost.timeStamp[i + 1])

            {

                index1 = i;

                index2 = i + 1;

                return;

            }

        }

        index1 = ghost.timeStamp.Count - 1;

        index2 = ghost.timeStamp.Count - 1;

    }



    private void SetTransform()

    {

        if (index1 == index2)

        {

            this.transform.position = ghost.position[index1];

            this.transform.rotation = ghost.rotation[index1];

        }

        else

        {

            float interpolationFactor = (timeValue - ghost.timeStamp[index1]) / (ghost.timeStamp[index2] - ghost.timeStamp[index1]);



            this.transform.position = Vector3.Lerp(ghost.position[index1], ghost.position[index2], interpolationFactor);

            this.transform.rotation = Quaternion.Slerp(ghost.rotation[index1], ghost.rotation[index2], interpolationFactor); 

        }

    }

}

There is also a basic interpolation happening just to smooth out any hiccups that may happen on playback and add a final smooth to the action.


At first I had set the data recording to be at 30 updates per second as to not make it so taxing on the game. However this was an issue and had to do with the way in which both my camera and player physics work. For my camera I use Cinemachine, which allows for many things such as camera swapping and cutscenes to be created easily. However as my player physics work on a fixed update cycle (because it is more accurate) it means the Cinemachine camera also has to be on fixed update in order to stop a visual stutter to occur on the player object when in a follow cam tied to the position of the player. This also means that any other objects moving at high speeds and not on fixed update display this stutter. And so my "npc's" have to also update at 50 times a second as that is Unity's fixed update cycle. This does mean more data but so far I haven't encountered any issues with slower performance.


So what this all meant is now I can play the level, record myself mutiple times for variation, and then play them back on npc racer models, allowing for competition to happen which is great. The few drawbacks is having to manually record and (update if needed) racers paths as well as them not reacting to you in terms of racing lines. You can still hit them and push them though and they will do the same back. Here is a recording of them in action. I purposely have left one recorded at 30 updates/s in white and the final one at 50/s in pink:



I then added controller support, at least for the xbox controller. I need to do further research on how to get it working with both ps4 and xbox. It was quite easy so far as all I had to do was set up an input manager and then use a template to link the right inputs to the right buttons.


I then finally set up the fly through starting cutscene for my first level. There are still a few clipping issues and tweaks that I want to iron out but for now having a working blueprint is what I am happy with. I spent time making sure it was unique and showed of parts of the level that will intrigue the player.




After that I decided to make a third level. I took heavy inspiration from Cowboy Bebop as well as Mass Effect with their space station designs and decided to create a race track that was part of a space station that acted as a city for a human population. I also upped the excitement by making this tracks gimmick be loop de loop's and twisty turns. This adds another tool into my complexity belt for track design within the game and helps to make each tracks experience more unique. I also want to experiment with boost pads later.


I started off first with very simple drawings in Photoshop before taking it into Maya and creating a curve for plane extrusion. However this track and it's loops ended up being very tricky for me as the curve tool only works on a single plane at a time which can be difficult to use when making complex shapes such as a spiral. And so I ended up having to divide the track into three sectors and then stitch them back together once complete.


After plenty tinkering my way of creating a spiral plane was to add the default spiral into Maya through it's primitive shapes. You can then select an edge and convert it into a curve giving us a spiral curve. However using plane extrusion here ends up with the plane rotation not staying flat, which isn't what we want for a race track and so I ended up using my original technique of lofting between two spiral curves which worked great!



Final track design:



As you can see, there are a LOT of polygons here. This was my first attempt at creating my collision mesh for the track. Usually I do it this way, by simply subdividing it for smoother physics then the visual track. However it really did not like any form of loop or spiral it while adding poly count, didn't really smooth any of the joints in the curve. This meant when playing on the track it was very jolting and not a fun experience. It also caused slower speeds allowing the player to only tack the spiral at ~55/100 speed. And so I went back to the original simpler mesh and used Maya's built in smoothing tool and Maya Catmull-Clark to create a far smoother, and also far lower poly count collision mesh.



Half as high polycount. I also could reach top speed now through the spiral making it far more enjoyable and shaving nearly 15 seconds off the lap time.


I then added walls, and started creating assets. The first being "The Iris", my space station colony creation. Say that four times fast.



As well as another sky box:


And new space ready road textures:



Here's the final level so far (still very WIP):



8 views0 comments

Recent Posts

See All

댓글


bottom of page