Skip to content

AntonC9018/race

Repository files navigation

Setup

  1. Install DMD to be able to compile and make use of the dev cli tools.
  2. Install .NET6 SDK to be able to compile and run the code generator.
  3. Install Unity 2021.2.12f1.
  4. Install Git LFS.
  5. Clone this repository git clone https://github.com/AntonC9018/race --recursive.
  6. Run setup.bat in the root directory.

IMPORTANT!

ALL of the above are absolutely necessary to be able to build the game.

Not running the script may mess up the Unity project, because autogenerated source files will be missing, which will make Unity delete the associated meta files, which could mess up the links between the different objects.

Also, install D before running it, because it just builds the dev tool and delegates all of the work to it.

If you forgot to run it, just roll back the git changes, run it, and then reopen the editor.

Additionally:

  • Install Blender to work with models.

Requirements

Create a small "1 v bot" drag racing game with 3rd person camera view.

Garage:

  • Change car color
  • Increase car HP
  • Add your username
  • Low Medium High graphics settings
  • Shop with 2 in-app purchases to buy coins [100 coins pack - $0.99, 200 coins pack $1,99]
  • Save all information in playerPrefs
  • Performance optimised UI is very important
  • When starting the race - choose 1 from 2 maps available

Ads:

  • Show an interstitial ad between first and second scene

Drag racing map (2 maps):

  • Use addressables to spawn cars
  • Movement: WASD [SPACE] + shift & ctrl to change gears
  • First to come to the finish line wins
  • After race is over - move back to first scene (garage)

Screenshots

Press F5 to switch view while in race.

Garage Notes

Some of the things I'd like to mention follow.

On the gameplay elements:

  • The only functional requirement that has not been implemented are the graphics settings. I had to scrap it for now to constrain the scope of the project.

  • You can open up the terminal by pressing the backtick key, or open it fully by pressing ctrl + backtick. Type help to get info on the available commands. (You can give youself coins or stats).

    The command terminal is my older project, which I have rewrote almost fully, having initially forked this project. I have integrated it with my code generator, but it still needs a lot of work and bug fixing to be of production quality.

  • The color picker selects the main color of the currently selected car. The color picker was initially forked from here, but I had to rewrite most of the script to simplify the code and to eliminate bugs.

  • After that we have a dropdown that selects one of the 2 cars available. The first car (Bad Car) I made myself in Blender (I'm pretty much a beginner, which is why it's so bad), the latter ones I downloaded online. I had to modify the downloaded models a little bit in Blender, like adjusting the hierarchy and the names. I only use manually exported FBX files in Unity, that is, I don't use direct import from Blender.

  • The stats can be changed once a car is selected. Initially, they are set to the base stats. In order to be able to change stats, you'll need to have stat value, which you could trade for stats. To get stat value, use the add_statvalue console command.

  • Under that we have the money counter. The coins are only used for bying stats right now, while the rubies are unused.

  • Under that we have the nickname text input. It's bound to the user data model and to the text field above the car.

  • Now, any changes made to any objects while the game is open are going to be saved. When you reopen the game, all those values will be restored to the ones they were when you closed it. The car stats and colors are saved to an xml file (see the paths printed to the console). The user nickname, the last selected car and the number of coins and rubies are saved manually to PlayerPrefs.

More on the code / design:

  • The experimental package I'm using is Unity.VectorGraphics to be able to diplay SVG's, but that's just me trying things. Vector graphics are nice, because they scale to any resolution, but their support in Unity is quite limited and buggy at the moment.

  • I kind of messed up with the event variable names. By habit, I started naming them with the first letter being capital, but that can confuse you if there are handler methods in the same class. Since they both start with On, you cannot distinguish a handler vs an event just judging by the name, which I have overlooked. I don't want to rename them right now, because it'll mess up the links in the editor (I could use [FormerlySerializedAs], but meh).

  • The code generator is actually currently not used that extensively. I do use it here and there, but in some places I don't because I just wanted to do this part quicker, so I didn't bother writing a Kari plugin (Kari is the name of the code generator).

  • Events infos (or contexts) are readonly structs! Even though there's no real benefit, since Unity would still box/copy them with their UnityEvent implementation, it can be mitigated to get actual zero-allocation, zero-copy contexts. See the concepts/Stackref folder for an example. Basically, it involves a wrapper that would store the pointer to the event context struct on the stack. And yes, it does work with managed structs too. So, ultimately, the plan is to write a Kari plugin to generate easy-to-use wrappers for them to impelment and hide all of the pointer business.

Actually, these structs will still be boxed, I think. I will need to test that, or study the source code.

  • No tests yet!

  • There are notes about singletons in the sources. I kind of dislike singletons, but I understand that they can be beneficial for some use cases, even though they provide implicit context which makes code less tractable. I'm using a simple dependency injection implementation so that I don't have to wire up stuff manually in the editor.

  • I'm not unhooking some callbakcs in OnDisable(). This is a conscious decision. The reason I'm not doing it is because I intend these pieces to either always be used in conjuction, or disabled/destroyed all at once. They should never be disabled on individual basis.

  • I'm using UnityEvents to connect everything together. I'm sure there is a library, or a more efficient (from the boilerplate point of view) way of programming this, but I just don't have enough experience in Unity to know that.

  • I know about the NotifyPropertyChanged pattern, I'm just not fond of it. I did mention this at some point in the code, but to me, triggering the callback manually after setting the value is not a big deal, and I'd rather have the added flexibility that that brings than be constrained by it invisibly updating at a wrong moment. I understand, that I could refactor the code more easily and reliably if I do that notify-immediately-on-set pattern, and would be less likely to miss it when I meant it, so I might refactor that, but to me, triggering the callback manually feels better.

Gameplay

  • I'm enabling both input systems, because the command terminal package uses the old input system.

  • Most of the heavy lifting for car movement is done by the built-in WheelCollider, while the engine simulation is custom. I have designed a gear based system that computes the current RPM of the engine from the current RPM of the wheels, then uses that to compute the efficiency of the engine, which is then used to compute and apply torque to the wheels.

  • The speedometer and the tachometer are created dynamically based on the properties of the selected car.

  • I'm using prefabs to represent scenes instead of using actual scenes. It makes the transition between scenes a lot simpler and more manageable. This may mean that the lighting is harder to control, but I have not looked deep enough into this.

  • The bot's logic is very minimal at this point. It tries to stay at the center of the road, without flipping over. It always puts the pedal to the metal and only drives in first gear. A better AI algorithm had to be scrapped to constrain the scope of the project.

  • The race management includes disabling and then respawning the participant if it leaves the road or happens to flip over, detecting when one of the participants reaches the end of the track.

  • The track itself is not limited to being linear. I made it so that the code uses the IStaticTrack interface. The classes that implement this interface are supposed to represent a track, aka a sequence of road segments. The position of the participants within the track is stored in road coordinates, which is a road segment number and a normalized position within such road segment. Whoever implements IStaticTrack convert to (from) world space coordinates to (from) these road space coordinates. Such system allows abstracting away the underlying track structure.

    Right now, I'm only using a StraightTrack implementation, which is just a single simple straight road segment, but you can imagine an implementation based on EasyRoads3D (scrapped for now), which would use spline equations to determine the middle position of a road segment, the curvature information that a bot will use to select a turning angle and the optimal speed. Slopes are technically supported too, because the code depends on normals and rotations obtained from this track interface.

  • The gameplay_playground scene contains a few editor helpers for setting up the car colliders and computing gear ratios from speeds.

Initialization

The initialization & transition is the most involved and messy part in my opinion.

Every "scene" is just a prefab. No new actual Unity scenes are created at runtime, all I do is enable or disable game objects.

This makes things a lot easier to manage:

  • I can have a playground scene for e.g. the gameplay, that will have a player pre-placed in the scene, without that propagating to the main scene, where I spawn the cars dynamically.

  • I can factor out the shared parts in a separate prefab, without having to worry about any "don't destroy on load" and singleton instances nonsense.

  • The static parts that will have to always get copied on "scene" instantiation can be shared between the main scene and the playground scene, by putting them in a prefab.

  • A playground scene can use a completely different initialization strategy than the one that used when that scene gets intantiated from the main scene. For example, when initializing the gameplay scene locally, the car that's already in the scene needs to be managed by user input, but when the gameplay scene is instantiated from the main scene, the cars should get created dynamically based on the information from the garage scene.

Let me mention some terminology that I settled on regarding this scene management approach:

  • shared things are the things that are reused across "scenes": the event system for UI and the command terminal are the two things that are shared currently.

  • core things are the static things that always need to exist for a scene to function correctly. For example, the UI, camera, lights.

  • core contains the initialization object (component). It implements an initialization interface, which takes data specific to that "scene", and performs some internal initialization. For example, for the gameplay "scene", the initialization object would inject the properties (wrapper for the data model that exposes callbacks for when the data changes) into the UI components, initialize the race logic based on the number of participants, create the bot input views (the difficulty settings are scrapped for now), etc.

To be clear, this initialization object does not know about the other scenes. All it could get is an abstract transition handler, that it could use to initiate the transition to other scenes. If the initialization outcome depends on the data from other scenes, it should be managed by some other entity.

Currently, there is a "God entity" that manages the aforementioned transferring data between scenes in the correct format. Right now I only need to transfer data from garage (the selected car, the color, the stats, etc.) to the gameplay scene, while converting it to the format that it understands. That means this class would spawn the "gameplay cars", that is, cars with colliders and the needed runtime data and whatnot, by instantiating the prefab that corresponds to the given "garage car". It would convert the current stats received from the garage scene into an adjusted car "spec" (motor power, the gear ratios, etc.), which it will pass with the car objects that the gameplay scene understands.

So, as you can imagine, this God entity is pretty much tightly coupled with all the other scenes, managing showing/hiding of the scenes, data transfer and data conversion. I could decouple it more later. At least the data conversion part ideally should be handled elsewhere.

So there are 2 kinds of initialization strategies: local and the initialization from main (let's just call it default).

The default initialization is initiated by the God entity, while the local one gets executed only while in the playground scene.

I've spent as much time trying to understand and implement this as I did coding the gameplay logic and figuring out the math involved in the engine simulation. This was by far the most complicated problem in this project.

Links