How to Make a Round System of a Fighting Game Like Street Fighter or Mortal Kombat (C#)

I have created a Street Fighter and I only did (Final Round) I wanted to do two rounds like Street Fighter or Mortal Kombat and I don’t know how to do it or script, that’s how I want it.


8509016--1133690--street-fighter--644x362.jpg

Usually a GameManager type of script would handle this, living through as many scenes as necessary until the game is won.

There are tons of tutorials for the concept of a GameManager, and lots of different implementations.

When you fully understand the basic concept of something called a GameManager that lives longer than a scene for this purpose, you are welcome to take a look at this code too:

ULTRA-simple static solution to a GameManager:

OR for a more-complex “lives as a MonoBehaviour or ScriptableObject” solution…

Simple Singleton (UnitySingleton):

Some super-simple Singleton examples to take and modify:

Simple Unity3D Singleton (no predefined data):

Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

These are pure-code solutions, DO NOT put anything into any scene, just access it via .Instance!

If it is a GameManager, when the game is over, make a function in that singleton that Destroys itself so the next time you access it you get a fresh one, something like:

public void DestroyThyself()
{
   Destroy(gameObject);
   Instance = null;    // because destroy doesn't happen until end of frame
}

There are also lots of Youtube tutorials on the concepts involved in making a suitable GameManager, which obviously depends a lot on what your game might need.

OR just make a custom ScriptableObject that has the shared fields you want for the duration of many scenes, and drag references to that one ScriptableObject instance into everything that needs it. It scales up to a certain point.

If you really insist on a barebones C# singleton, here’s a highlander (there can only be one):

And finally there’s always just a simple “static locator” pattern you can use on MonoBehaviour-derived classes, just to give global access to them during their lifecycle.

WARNING: this does NOT control their uniqueness.

WARNING: this does NOT control their lifecycle.

public static MyClass Instance { get; private set; }

void OnEnable()
{
  Instance = this;
}
void OnDisable()
{
  Instance = null;     // keep everybody honest when we're not around
}
1 Like

Once you make a singleton (let’s use Kurt’s example above), here’s the logic, exactly how it works in MK, but you can change it if you’d like.

public class RoundTracker : MonoBehaviour {

  private const int WINS_REQUIRED = 2;

  public enum Side {
    Left = 0,
    Right = 1
  }

  static public RoundTracker Instance { get; private set; }

  int _leftWins, _rightWins;

  void Awake() => Reset();
  void OnEnable() => Instance = this;
  void OnDisable() => Instance = null;
  void OnDestroy() => OnDisable();
  public void Reset() { _leftWins = _rightWins = 0; }

  public void RecordRoundWinner(Side winner)
    => setWins(winner, getWins(winner) + 1);

  public int CurrentWinsOf(Side side) => getWins(side);

  public int TotalRounds => _leftWins + _rightWins;

  public bool FinalRound()
    => _leftWins == WINS_REQUIRED || _rightWins == WINS_REQUIRED;

  public Side FinalWinner => _leftWins > _rightWins? Side.Left : Side.Right;

  private int getWins(Side side)
    => side == Side.Right? _rightWins : _leftWins;

  private void setWins(Side side, int value) {
    if(side == Side.Right) _rightWins = value;
      else _leftWins = value;
  }

}

Then someplace else you record the outcome of each round by doing

RoundTracker.Instance.RecordRoundWinner(winner: RoundTracker.Side.Left);

Then you check for the overall winner before you proceed with the next round

var rt = RoundTracker.Instance;
if(rt.FinalRound()) {
  var winsTally = $"{CurrentWinsOf(RoundTracker.Side.Left)} to {CurrentWinsOf(RoundTracker.Side.Right)}";
  Debug.Log($"After {rt.TotalRounds} rounds, the winner of the " +
            $"match was the player on the {rt.FinalWinner:G}, " +
            $"with the score of {winsTally}.");
}

Something like that.

Edit:
fixed a typo
Edit2:
added public to Reset, kind of important

4 Likes

thanks for your help, orionsyndrome

thanks to you too for your help, Kurt-Dekker

1 Like

i will try.

And if you want intentionally to support a match draw (I think MK doesn’t do that), then you’d have to change this to

// this can now return null as well, it's called a nullable value
public Side? FinalWinner {
  get {
    if(_leftWins > _rightWins) return Side.Left;
      else if(_rightWins > _leftWins) return Side.Right;
    return null;
  }
}

Then you gotta check for this special case afterward by doing this

var rt = RoundTracker.Instance;
if(rt.FinalRound()) {
  var winner = rt.FinalWinner;
  var winsTally = $"{CurrentWinsOf(RoundTracker.Side.Left)} to {CurrentWinsOf(RoundTracker.Side.Right)}";
  if(winner.HasValue) {
    Debug.Log($"After {rt.TotalRounds} rounds, the winner of the " +
              $"match was the player on the {winner.Value:G}, " +
              $"with the score of {winsTally}.");
  } else { // it's a draw
    Debug.Log($"After {rt.TotalRounds} rounds, the match was a draw, " +
              $"with the score of {winsTally}.");
  }
}

Of course this assumes that you also modify FinalRound() to something specific, for example

public bool FinalRound() => TotalRounds == MAX_ROUNDS ||
   Mathf.Abs(_leftWins - _rightWins) == WINS_REQUIRED;

That makes sense, right?
And then you add

private const int MAX_ROUNDS = 4; // as an example

This would allow it to behave like a tennis match of sorts. Or a volley ball, or something like that, I don’t follow any kind of sports any more.

I’m seeing right now a couple more implications. You would have to think more thoroughly to come up with a fully-fledged logic, but this is all the code that you need. (Edit: I’ve modified this code slightly to make sure it works, see edit below for explanation.)

If you need something specific, feel free to ask, I will modify this to accommodate for it.

Edit:
I’ve revisited this variant, it should work like this: the winner is whoever has the advantage of 2 wins, after round 4 the match is closed, and if the wins are equal then the match ends in a draw.
Edit2:
Final variant of this in Post #18

1 Like

CS0103 The name ‘side’ does not exist in the current context

8509208--1133744--Captura de pantalla 2022-10-12 193939.png

@TheFighterSFMK2022
It’s more helpful if you give me the exact line where this error shows up. You got the line number on the right, and in general, you just double click on that, and it’ll show you. Anyway, I made a typo with this method

public void RecordRoundWinner(Side winner)
    => setWins(winner, getWins(winner) + 1); // should be winner, not side

didn’t work,
CS0111 The type ‘RoundTracker’ already defines a member named ‘RecordRoundWinner’ with the same types of.

Oh, don’t forget you can call Reset() to reset the whole thing! It also works in the inspector (in the top right corner).

Although I forgot to make it public if you need to call it from code.

public void Reset() ... // just add public to the existing method

And the call itself looks like this

RoundTracker.Instance.Reset();

You need to replace the old one, can’t keep both of them.

Your explanation is good, but I am having a series of many script problems.

I’m glad mine is good, because your explanation is bad :smile:
How am I supposed to fix it? If you don’t tell me what’s wrong I don’t have time to run this in Unity atm, maybe tomorrow.

var rt = RoundTracker.Instance;
if(rt.FinalRound()) {
  var winner = rt.FinalWinner;
  var winsTally = $"{CurrentWinsOf(RoundTracker.Side.Left)} to {CurrentWinsOf(RoundTracker.Side.Right)}";
  if(winner.HasValue) {
    Debug.Log($"After {rt.TotalRounds} rounds, the winner of the " +
              $"match was the player on the {winner.Value:G}, " +
              $"with the score of {winsTally}.");
  } else { // it's a draw
    Debug.Log($"After {rt.TotalRounds} rounds, the match was a draw, " +
              $"with the score of {winsTally}.");
  }
}

I don’t know where to put it, because I get dizzy with those codes, I want to complete the script because I have errors that I can’t solve, when I finish it I will understand the complete RoundTracker script.

[código=CSharp]usando System.Collections;
usando System.Collections.Generic;
utilizando UnityEngine;

clase pública RoundTracker: MonoBehaviour
{

privado const int WINS_REQUIRED = 2;
privado const int MAX_ROUNDS = 4;
Lado de enumeración pública
{
Izquierda = 0,
Derecha = 1
}

Instancia de RoundTracker pública estática { get; conjunto privado; }

int _leftWins, _rightWins;

void Despertar() => Restablecer();
void OnEnable() => Instancia = esto;
void OnDisable() => Instancia = nulo;
void OnDestroy() => OnDisable();
void Restablecer () { _leftWins = _rightWins = 0; }

public void RecordRoundWinner(Ganador del lado) => setWins(lado, getWins(lado) + 1);

public int CurrentWinsOf(Side side) => getWins(side);

public int TotalRounds => _leftWins + _rightWins;

bool público FinalRound()
=> _leftWins == WINS_REQUIRED || _rightWins == GANANCIAS_REQUERIDAS;

public Side FinalWinner() => _leftWins > _rightWins ? Lado.Izquierdo: Lado.Derecho;

getWins privado int (lado lateral)
=> lado == Lado.Derecha ? _rightWins : _leftWins;

setWins de vacío privado (Lado lateral, valor int)
{
if (lado == Lado.Derecho) _rightWins = valor;
else _leftWins = valor;

}

Restablecer vacío público ()
{
RoundTracker.Instance.Reset();
}

Lado publico? Ganador final
{
obtener
{
if (_leftWins > _rightWins) devuelve Side.Left;
de lo contrario, si (_rightWins > _leftWins) devuelve Side.Right;
devolver nulo;
}
}
public bool FinalRound() => TotalRounds == MAX_ROUNDS ||
_leftWins >= WINS_REQUIRED || _rightWins >= WINS_REQUIRED;
}[/código]

I don’t know what’s going on I can’t upload the script (archive).

it goes again

8509286–1133771–RoundTracker.cs (1.54 KB)

You’ve done it again, you can’t have the same method twice :slight_smile: !
If I said ‘replace’ that means delete the old one, i.e. copy/paste over it.

Maybe it’s a bad practice on my part, but with coding in general, you can’t have duplicate items, unless you really want to access the same method in different ways, but in this case we don’t want any duplicate items, mostly because I don’t really want to complicate things.

So if you added the last variant with 4 max rounds, and introduced the changed method FinalRound(), you absolutely have to replace the old one! It’s on the line 32. This goes for FinalWinner property as well, it’s already on line 35!

Btw I have also updated this code (the ‘max rounds’ variant).

So here it is again in its entirety, to try and calm the confusion down.

using UnityEngine;

public class RoundTracker : MonoBehaviour {

  private const int MAX_ROUNDS = 4;
  private const int ADVANTAGE_REQUIRED_TO_WIN = 2;

  public enum Side {
    Left = 0,
    Right = 1
  }

  static public RoundTracker Instance { get; private set; }

  int _leftWins, _rightWins;

  void Awake() => Reset();
  void OnEnable() => Instance = this;
  void OnDisable() => Instance = null;
  void OnDestroy() => OnDisable();
  public void Reset() { _leftWins = _rightWins = 0; }

  public void RecordRoundWinner(Side winner)
    => setWins(winner, getWins(winner) + 1);

  public int CurrentWinsOf(Side side) => getWins(side);

  public int TotalRounds => _leftWins + _rightWins;
  public int CurrentAdvantage => Mathf.Abs(_leftWins - _rightWins);

  public bool FinalRound()
    => TotalRounds == MAX_ROUNDS ||
       CurrentAdvantage == ADVANTAGE_REQUIRED_TO_WIN;

  public Side? CurrentWinner {
    get {
      if(_leftWins > _rightWins) return Side.Left;
        else if(_rightWins > _leftWins) return Side.Right;
      return null;
    }
  }

  private int getWins(Side side)
    => side == Side.Right? _rightWins : _leftWins;

  private void setWins(Side side, int value) {
    if(side == Side.Right) _rightWins = value;
      else _leftWins = value;
  }

}

Make sure you call the proper method in proper times.

After the round is finished, don’t forget to call RecordRoundWinner and assign the winner.
Then, evaluate the match by checking first if FinalRound() is true (and everything else according to post #7).
After the match is evaluated and the winner is determined (or it’s a draw), don’t forget to call Reset().

The calls work like this, you can call from anywhere, assuming this script is alive and on some gameobject in the scene.

RoundTracker.Instance.FinalRound();

You can also store this instance locally, so that you don’t have to repeat yourself all the time.

void myFunction() {
  var tracker = RoundTracker.Instance;
  tracker.RecordRoundWinner(RoundTracker.Side.Right);
  if(tracker.FinalRound()) {
    // etc.
  }
}

I’ve also added CurrentAdvantage property which you can evaluate on a round-to-round basis, if you want to.

Finally, you can easily revert to classic MK behavior by setting MAX_ROUNDS to 3.
This will prevent draws on its own and you won’t have to change anything else.

I got the same error message again
CS0103 The name ‘side’ does not exist in the current context

If you’re tracking your winners differently (for example 0 and 1) you can just do

int winnerIndex = 1;
tracker.RecordRoundWinner((RoundTracker.Side)winnerIndex);

Or if you really like to be verbose about this

int winnerIndex = 1; // or 0, it's probably some argument anyway
switch(winnerIndex) {
  case 1: tracker.RecordRoundWinner(RoundTracker.Side.Right); break;
  default: tracker.RecordRoundWinner(RoundTracker.Side.Left); break;
}

That’s because you gave me code with this error still in there.
Now fix it on your own, hopefully you’ve learned what was wrong before.