The Mechanic That Was Fully Explained

The Turtle Who Had Wings Logo (Placeholder 3)
Made another quick placeholder, since the artist hasn’t gotten to the logo yet, and the previous placeholder lacked the subtitle.

Every time I talk to someone who isn’t a coder about coding, they seem to have no idea how easy or hard it is to code a game, either thinking it’s something simple or feeling overwhelmed by the whole thing. At the same time, I’ve never fully explained how the Sync mechanic works in The Turtle Who Had Wings. So I may as well explain the mechanic in-depth by showing exactly how the code calculates it, right?

So, let’s get started. As I explain what the code does, try going back to read it and see if you can piece together what was going on. I’m not going to dwell on it, because I’m a terrible teacher.

As you may or may not know, the Sync mechanic is a mechanic that involves the player switching between two characters at regular intervals when in combat in order to maintain a value between 100% and -100%, which affects stats among other things. In combat being the key thing to figure out before anything else – the player can switch however they like outside of combat. So, how do I define it? Given the limitations of the game’s genre, I chose to restrict it by if any enemies have noticed and are chasing the player.

This is always running in order to determine that:

    void DidSomeoneNotice() {
        SomeoneNoticed = false;
        foreach (GameObject Enemy in GameObject.FindGameObjectsWithTag("Enemy")) {
            if (!SomeoneNoticed) {
                if (Enemy.GetComponent<Chase>() != null) {
                    if (Enemy.GetComponent<Chase>().Noticed == true) {
                        if (Vector3.Distance(PlayerA.transform.position, Enemy.transform.position) <= (PlayerA.renderer.bounds.extents.magnitude * 200)) {
                            SomeoneNoticed = true;
                        } else if (Vector3.Distance(PlayerB.transform.position, Enemy.transform.position) <= (PlayerB.renderer.bounds.extents.magnitude * 200)) {
                            SomeoneNoticed = true;
                        }
                    }
                }

                if (!SomeoneNoticed) {
                    GameObject[] Players = GameObject.FindGameObjectsWithTag("Player");
                    foreach (GameObject Player in Players) {
                        RaycastHit PlayerHit;
                        if (Physics.Raycast(new Ray(Enemy.transform.position, (Player.transform.position - Enemy.transform.position)), out PlayerHit, (Player.renderer.bounds.extents.magnitude * 50))) {
                            if (PlayerHit.collider.gameObject == Player) {
                                SomeoneNoticed = true;
                            }
                        }
                    }
                }
            }
        }
    }

What this does is that it checks for every enemy that is able to sense nearby player characters (rather than stationary enemies that don’t actively fight the player), and asks if any of them have noticed the player. If they have, it checks for line of sight to see if the player is right in front of them at that exact moment. Not the most efficient way to do such a calculation, but it works.

The next thing that needs to be calculated is the range at which it is acceptable for the player to switch between the two characters they’re controlling. This is actually fairly basic math:

    void AdjustPerfectSync() {
        if (LastSyncIndex == SyncHistory.Length) {
            LastSyncIndex = 0;
        }

        float RunningAverage = 0;
        float HighestSync = 0;
        float LowestSync = 0;
        for (int i = 0; i < SyncHistory.Length; i++) {
            RunningAverage += SyncHistory[i];
            if ((HighestSync == 0) || (SyncHistory[i] > HighestSync)) {
                HighestSync = SyncHistory[i];
            }

            if ((LowestSync == 0) || (SyncHistory[i] < LowestSync)) {
                LowestSync = SyncHistory[i];
            }
        }

        ArrayList SortedSyncHistory = new ArrayList();
        SortedSyncHistory.AddRange(SyncHistory);
        SortedSyncHistory.Sort();

        float AverageSync = ((RunningAverage - (float)SortedSyncHistory[0] - (float)SortedSyncHistory[1] - (float)SortedSyncHistory[18] - (float)SortedSyncHistory[19]) / (SyncHistory.Length - 2));
        DefaultSyncPerfectPoint = AverageSync;
        if (((HighestSync - LowestSync) > (AverageSync * 2)) || ((HighestSync - LowestSync) < (AverageSync / 2))) {
            SyncGracePeriod = AverageSync;
        } else {
            SyncGracePeriod = (HighestSync - LowestSync);
        }

        SyncEarlyPoint = (DefaultSyncPerfectPoint - (SyncGracePeriod / 2));
        SyncLatePoint = (DefaultSyncPerfectPoint + (SyncGracePeriod / 2));
        SyncPenaltyPoint = (DefaultSyncPerfectPoint + SyncGracePeriod);
    }

For each pair of player characters, the game saves the timing of the player’s last 20 switches (30 seconds by default for all 20 slots, though I may change that to something shorter based on recent tests). The game will update what the latest switch’s interval was later, but here it’s just calculating based on previous switches. Taking an average of the past 20 switches (removing the two highest and two lowest in case of outliers), the game makes that the perfect time to switch between the two characters (game logic being that it’s what the characters are “used to”).

Then, a range in which the player has to hit is calculated from either half the average, or from the difference between the longest and shortest intervals (defaulting to the latter unless the difference is more than double or less than half of the average). This range is then calculated with the perfect time in the middle, setting what the boundary is for being too early and too late, and then doubles the distance between being perfect and being too late to determine the point at which it will start punishing the player for completely ignoring the mechanic.

So now we need the code to calculate how long the player has been gone being noticed and not switching, and start/stop the penalty countdown if necessary. Simple enough:

    void SyncTimeDifference() {
        if (SomeoneNoticed) {
            SecondsSinceLastSwap += Time.deltaTime;
            NoticeTime += Time.deltaTime;
        } else {
            SecondsSinceLastSwap = 0;
            SafeTime = 0;
            NoticeTime = 0;
        }

        SyncPerfectPoint = (DefaultSyncPerfectPoint + (((SyncGracePeriod / 2) - 2) * SyncPoints / MaxSync));

        if ((SecondsSinceLastSwap > SyncPenaltyPoint) && (SyncPoints != -MaxSync) && SomeoneNoticed) {
            if (!PenalizingSync) {
                StartCoroutine("SyncPenalty");
            }
        } else {
            StopCoroutine("SyncPenalty");
            PenalizingSync = false;
        }
    }

    IEnumerator SyncPenalty() {
        PenalizingSync = true;
        float Penalty = 0;
        while (SecondsSinceLastSwap > SyncPenaltyPoint) {
            Penalty += 10 * Time.deltaTime;
            SyncPoints -= (Penalty * Time.deltaTime);

            if (SyncPoints <= -MaxSync) {
                StopCoroutine("SyncPenalty");
                PenalizingSync = false;
            }

            yield return null;
        }

        if (SecondsSinceLastSwap <= SyncPenaltyPoint || SyncPoints == -MaxSync) {
            StopCoroutine("SyncPenalty");
            PenalizingSync = false;
            yield return null;
        }
    }

The penalty is about as straightforward as everything else, being a penalty that starts off slowly lowering your Sync, but the amount it lowers per second by is also growing as time passes, so the penalty will just keep rising until it eventually lowers your Sync to the lowest possible value of -100%. Not to mention that, just to trigger this, the player has to be really late at switching, so they’ll receive an additional penalty when they do switch for being late. You really don’t want to get to the point where the penalty is even running for a moment, but if you do, switch immediately to minimize the damage.

Since this could get really confusing for players, there’s an additional part to this that was added to make sure the player doesn’t overshoot unknowingly and knows the risk they’re taking:

    void GenerateSafeTime() {
        if (SomeoneNoticed && (SafeTime == 0) && (NoticeTime >= 0.5f)) {
            while ((SafeTime == 0) || ((SafeTime != 0) && (Mathf.Abs(SafeTime - SyncPerfectPoint) > (SyncGracePeriod / 4)))) {
                SafeTime = Random.Range(SyncEarlyPoint, SyncLatePoint);
            }

            SafeTime += (Time.timeSinceLevelLoad - SecondsSinceLastSwap);

            float SafeMinutes = Mathf.Floor(SafeTime / 60);
            float SafeSeconds = Mathf.Floor(SafeTime - (SafeMinutes * 60));
            float SafeMilliseconds = Mathf.Floor((SafeTime - Mathf.Floor(SafeTime)) * 1000);

            SafeString = "SAFE ";
            if (SafeTime > SyncPerfectPoint) {
                SafeString += "BEFORE";
            } else {
                SafeString += "AFTER";
            }

            SafeString += ": ";
            if (SafeMinutes < 10) {
                SafeString += "0";
            }

            SafeString += SafeMinutes;
            SafeString += ":";
            if (SafeSeconds < 10) {
                SafeString += "0";
            }

            SafeString += SafeSeconds;
            SafeString += ".";
            if (SafeMilliseconds < 100) {
                SafeString += "0";
                if (SafeMilliseconds < 10) {
                    SafeString += "0";
                }
            }

            SafeString += SafeMilliseconds;
        }
    }

While the part where the text this generates is shown to the player isn’t included here, it should still be fairly clear what this is doing. Each time the player is freshly noticed, after a short period, a time that won’t result in a penalty is calculated (and recalculated until it’s not so close to perfect so as to give a lot of Sync from following it). Next, it checks whether the time is before or after the perfect time, and then writes a line of text saying “SAFE BEFORE/AFTER: ##:##:###” as applicable. If the player follows this without going too far in the other direction, they get a small boost when switching.

And then it all comes together:

    public void SwitchCharacters() {
        if (CanSwitch) {
            Defending = false;
            CurrentCharacterIsA = !CurrentCharacterIsA;
            Switching = true;

            if (SomeoneNoticed) {
                if (SecondsSinceLastSwap < SyncEarlyPoint) {
                    SyncPoints -= -((1 / (0 - (100 / (SyncEarlyPoint - SecondsSinceLastSwap)))) * 500);
                } else if ((SecondsSinceLastSwap >= SyncEarlyPoint) && (SecondsSinceLastSwap <= SyncLatePoint)) {
                    if (SecondsSinceLastSwap < SyncPerfectPoint) {
                        SyncPoints += (100 / (SyncPerfectPoint - SecondsSinceLastSwap));
                    } else if (SecondsSinceLastSwap == SyncPerfectPoint) {
                        SyncPoints = MaxSync;
                    } else if (SecondsSinceLastSwap > SyncPerfectPoint) {
                        SyncPoints += (100 / (SecondsSinceLastSwap - SyncPerfectPoint));
                    }
                } else if (SecondsSinceLastSwap > SyncLatePoint) {
                    SyncPoints -= -((1 / (0 - (100 / (SecondsSinceLastSwap - SyncLatePoint)))) * 500);
                }

                SyncHistory[LastSyncIndex] = SecondsSinceLastSwap;
                LastSyncIndex++;
                SafeTime = 0;
            }

            SecondsSinceLastSwap = 0;
        }
    }

Calculating from all of the variables set up earlier, the game determines whether the player’s Sync should be raised or lowered, and acts accordingly. Then it records the interval for this switch to the player’s record that was used to determine the average earlier, thus meaning that the perfect time to switch will theoretically be different next time, and adjust over time however the player wishes.

Sync also rises by 0.01% for each enemy defeated, and there’s a penalty for dying, but that’s mostly how it’s calculated.

Sync then goes on to be a third of the multiplier total (so hitting -100% will lower your stats by 33%, and hitting 100% will raise it by 33% – the only way to go over a multiplier of ×1.00). A pretty huge deal, if you ask me. Additionally, the player only has access to a combination attack that defeats all enemies within a certain radius if they have a Sync higher than 0% and if both characters have stamina over half, which costs 50% off of Sync and both stamina totals.

So yeah. Hopefully you came out of this post understanding exactly how the Sync mechanic works.

Advertisements

Feel free to comment, but be civil.

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s