This week I continued to work on getting the rest of the game mechanics polished so that I have a finalised game I can then expand on without worry. My main focus was getting a position counter working, allowing the player to know what position they are in during the race. As my HUD is quite unique I had a hard time trying to find a place to put the position HUD and so later on I solve it as you will see.
The way it works is by having a trigger placed on the straight at the finish line. As objects pass through it it then looks for a tag on the object. At first any tags were allowed, however as each racer and player are made of multiple components this ended up breaking the tag counting system in place. I fixed this by creating an allowed tag list, where I can define what tags to register and what to ignore:
private void OnTriggerEnter(Collider other)
{
string racerTag = other.tag;
if (!allowedTags.Contains(racerTag))
{
return;
}
if (!racerTriggerCounts.ContainsKey(racerTag))
{
racerTriggerCounts[racerTag] = 0; // start trigger count for the racer if not already present
}
// start trigger count for the racer
racerTriggerCounts[racerTag]++;
racerTriggerTimes[racerTag] = Time.time; // Store current time
Debug.Log($"Tag: {racerTag}, Trigger Count: {racerTriggerCounts[racerTag]}");
UpdatePositionText(racerTag);
}
And so it then adds one count to the racers total count, this essentially acts like a lap counter. However to make sure it knows who is in first when multiple racers are on the same 'lap',
script I utilised a namespace called LINQ. It allows you to do many filtering operations and was something shown to me by my computer science friend. This allows me to order the list of tags first by trigger count (the amount of times passed through the trigger), and then by the order in which they went through the trigger, with the older being placed in first position. This then means if two racers are on the same lap the one that went through first will be placed in first correctly. Here is how the code looks for this operation:
private void UpdatePositionText(string racerTag)
{
rankedRacers = racerTriggerCounts
.OrderByDescending(pair => pair.Value) // First order by trigger count (higher is better)
.ThenBy(pair => racerTriggerTimes[pair.Key]) // Then order by last trigger time (earlier is better)
.Select(pair => pair.Key)
.ToList();
As you can see within the code this list is then outputted to a text, that then appears on the players screen like this:
Once I had the order of racers in the correct positions I then worked on cleaning up the display and customising what will be outputted for the player to see. It was also at this point I decided that I wanted to keep this information off the screen, and rather display it within the world itself, leading to both a clearer player HUD and more immersive world. It also is more accurate this way as the list only gets updated once per lap due to one trigger and so passes done within the lap wouldn't take effect on the screen HUD. This way acts more true to life such as how pit boards work within real motor sport.
I made this change by adding the text to a world space canvas and placing it on top of the start/finish line, allowing the player to be updated once per lap:
As you can see I also made suffix's work, through the use of cases:
private string GetPositionSuffix(int position)
{
switch (position)
{
case 1: return "st";
case 2: return "nd";
case 3: return "rd";
default: return "th";
}
}
As there are not going to be more than 10 racers I don't have to worry about the teens. Another thing I did was then make the text only display fro a certain amount of time through a coroutine. This is due to the text being a UI element and so it would be rendered on top of everything else meaning when the player would see it across the map. This way the player sees it and then it's turned off until they pass through the trigger again.
// Update the TextMeshProUGUI component with the player's position
if (positionTextMeshPro != null)
{
string playerTag = "Player";
int playerPosition = rankedRacers.IndexOf(playerTag) + 1; // Find the player's position (1-based index)
positionTextMeshPro.text = $"{playerPosition}{GetPositionSuffix(playerPosition)}";
// Show the text for a certain duration
if (racerTag == playerTag)
{
StartCoroutine(ShowTextTemporarily(displayDuration));
}
}
}
private IEnumerator ShowTextTemporarily(float duration)
{
positionTextMeshPro.gameObject.SetActive(true);
yield return new WaitForSeconds(duration);
positionTextMeshPro.gameObject.SetActive(false);
}
After this all worked I then wanted to incorporate it into the end screen for each race. As I want my game to have lots of personality, depending what position you place will lead to a different animation of the player character playing. This is inspired by MX vs ATV Reflex, where if you don't place on the podium, it instead shows you walking away out of the stadium while others have their glory.
My first task was to be able to hand the final position count to the game manager which I did by adding this to the position script:
public string GetPlayerPositionText()
{
int playerPosition = GetPlayerPosition();
return $"{playerPosition}{GetPositionSuffix(playerPosition)}";
}
I then access that through the game manager scrip and make a private void position manager. Once the race finishes I then use it to grab the position and display it on the end screen:
void ShowFinalPosition()
{
if (positionManager != null && finalPositionText != null)
{
string playerPositionText = positionManager.GetPlayerPositionText();
finalPositionText.text = $"{playerPositionText}";
int playerPosition = positionManager.GetPlayerPosition();
// Activate the appropriate object based on the player's final position
switch (playerPosition)
{
case 1:
firstPlaceObject.SetActive(true);
break;
case 2:
secondPlaceObject.SetActive(true);
break;
case 3:
thirdPlaceObject.SetActive(true);
break;
default:
losingObject.SetActive(true);
break;
}
}
}
As you can see I once again use cases to define when to activate the certain animations I want to play. I've decided to have first, second, and third, and then a losing animation. Here's how it looked at this current stage:
For now the smiley face is acting as the game object for first place. After this I then grayboxed the area in which the animation will take place. It's a simple backdrop that sits under the map in each area:
I had to be sneaky in the space level and place it behind the space station. In order to teleport to this area after the race ends I then just created a new camera and set its priorty to be higher than that of the main follow cam and turned it off. I then added a line of code to the game manager that turns it on once the race is finished:
yourCinemachineVirtualCamera.gameObject.SetActive(true);
Simple and works like a charm. Here's how it looks when you win:
And when you lose:
As this level has no other racers the text returns to it's default state of 0. However note the smiley changing to signify that the cases are working.
The final thing I did this week was some simple optimization by just turning all environmental objects to stationary, lowering the batch count in some levels to ~25.
コメント