Demo

Screenshots and videos

Use case

Shiny Stats main configuration

Attributes:

  • Stamina
  • Strength
  • Intellect
  • Agility

Classes:

  • Warrior
  • Paladin
  • Rogue
  • Wizard
  • Cleric

Stats:

  • Health Points
  • Mana Points
  • Attack Power
  • Spell Power
  • Attack Speed
  • Walking Speed

Extra Stats:

  • Experience - Amount of experience points to gather in order to level up
  • Experience Drop - Amount of experience points to drop on entity death

Level and Experience:

  • Level 1 to 20
  • Use the built-in Experience System of Shiny Stats

Meta

Stat Affected by (Attribute)
Health Points Stamina
Mana Points Intellect
Attack Power Strength
Spell Power Intellect
Attack Speed Agility
Walking Speed Agility

Stats-Attributes interactions

Class Health Points (Stamina) Mana Points (Intellect) Attack Power (Strength) Spell Power (Intellect) Attack Speed (Agility) Walking Speed (Agility) Sum
Warrior ++ + +++ + ++ + 10
Paladin +++ ++ ++ ++ ++ + 11
Rogue + + ++ ++ +++ +++ 12
Wizard + +++ + +++ + + 9
Cleric ++ +++ + ++ + + 10

Class balance idea

Gameplay

There are two type of attack:

  • regular hit - Gives physical damage (using attack power stat). Trigger with mouse LEFT click
  • special skill - Depends on the class, and cost mana. Trigger with mouse RIGHT click
Class Skill Description
All Attack Regular attack using Attack Power
Warrior Fatal Strike 150% regular attack damage
Paladin Holy Strike 100% regular attack damage, the damage dealt are returned as Health Points
Rogue Backstab 75% chance to deal 100% regular attack damage, 25% chance to deal 500% regular attack damage
Wizard Flame Strike Use Spell Power instead of Attack Power to deal damage
Cleric Self Heal Use Spell Power to heal himself, no damage dealt to enemies
Difficulty to defeat Enemy lvl 1-4 Enemy lvl 5-9 Enemy lvl 10-14 Enemy lvl 15-19 Boss (lvl 20)
Player level 1-4 medium hard - - -
Player level 5-9 easy medium hard - -
Player level 10-14 - easy medium hard -
Player level 15-19 - - easy medium hard
Player level 20 - - - easy medium

Class configuration

Reference

Class Stamina Strength Intellect Agility Sum
Warrior 10 15 5 7 37
Paladin 15 6 9 7 37
Rogue 5 8 9 15 37
Wizard 7 5 18 7 37
Cleric 12 5 13 7 37

in Shiny Stats

Classes configured in the Shiny Stats Unity asset

Stats configuration

Experiences stats

The Experience and Experience Drop stats will be the easiest stats to configure. We will use arbitrary values for each level so it requires a single kill to level up from 1 to 2 and increase the difficulty gradually so there is ~10 enemies to grind at level 19 to reach 20.

Reference

Level(s) Experience Drop (on enemy’s death) Experience (required to level up the player) Expected kills to level up
1-4 80 + Level * 10 100 1 - 2 enemies
5-9 80 + Level * 10 250 2 - 4 enemies
10-14 80 + Level * 10 500 3 - 6 enemies
15-18 80 + Level * 10 1000 5 - 6 enemies
19 80 + Level * 10 2000 10 - 11 enemies
20 1500 (the boss) - -

in Shiny Stats

Experience stat configured in the Shiny Stats Unity asset

Walking Speed stat

Walking speed will marginally increase with the level, but is extremely influenced by the initial Agility attribute of the character. The Rogue will walk way faster than other classes.
The funny mechanism that I certainly want to introduce is that the player won’t be able to outrun a rogue enemy (unless the player picked Rogue too).

Walking Speed = 20 + Agility + Level / 10

Rogue class

Shiny Stats preview for the Rogue class

Other classes

Shiny Stats preview for the Wizard class

in Shiny Stats

Shiny Stats stat configuration

Health and Mana stats

We will also use a linear function for Health and Mana to increase constantly over the levels. Unlike the walking speed, the Health and Mana will be mainly influenced by the level.

Health Points = 20 + Stamina + Stamina * (Level - 1) / 4

Mana Points = Intellect + Intellect * (Level - 1) / 2

Attack & Spell stats

The remaining stats will use logarithms-like functions so the level affects them less and less. The mechanics I want to introduce is that the Health points will increase more than the Attack Power, Spell Power and Attack Speed in the top levels, to extend the combats duration in the end game.

Attack Power = Strength / 5 + Strength * log(Level, 10)

Spell Power = Intellect / 5 + Intellect * log(Level, 10)

The attack speed case

The Attack Speed is way more connected into the game and might mess around the animations and the experience overall. To be fair, I didn’t plan very well for attack speed variation in the gameplay. So we will leave this as a constant in Shiny Stats.

Attack Speed = 0.2

This will be the minimum, constant, period of time in seconds between two consecutive attacks regardless the class and level.

C# scripts

Player Movement

We will use the CharacterController component from Unity to handle the player’s movements. The initial code, without the extra game management logic and Shiny Stats, looks like the code below.

using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    private CharacterController CharacterController;

    private void Awake()
    {
        CharacterController = GetComponent<CharacterController>();
    }

    void FixedUpdate()
    {
        var moveHorizontal = Input.GetAxisRaw("Horizontal");
        var moveVertical = Input.GetAxisRaw("Vertical");
        var movement = new Vector3(moveHorizontal, 0F, moveVertical).normalized;

        CharacterController.Move(movement * Time.fixedDeltaTime * 0.2F);
        if (movement != Vector3.zero)
            transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(movement), 0.5F);
    }
}

Health System

The health system will cover the characters life and death.
We reload the scene to restart the game when the player dies. When this script is attached to an enemy, it will give experience points to the player on death.

using ShinyStatsn;  // Enable ShinyStats.
using UnityEngine;
using UnityEngine.SceneManagement;

[RequireComponent(typeof(ShinyStatsEntity))]
public class HealthSystem : MonoBehaviour
{
    public double MaxValue;
    public double CurrentValue;

    private ShinyStatsEntity ShinyStatsEntity;

    private void Awake()
    {
        ShinyStatsEntity = GetComponent<ShinyStatsEntity>();
        // Subscribe to the OnLevelChanged event.
        ShinyStatsEntity.OnLevelChanged += ShinyStatsEntity_OnLevelChanged;
    }

    /// <summary>
    /// Callback from ShinyStatsEntity when the Level change.
    /// </summary>
    private void ShinyStatsEntity_OnLevelChanged(object sender, EventArgsInt e)
    {
        MaxValue = ShinyStatsEntity.EvaluateStat("Health Points");
        CurrentValue = MaxValue; // Reset to 100% HP on level up.
    }

    /// <summary>
    /// Handle the damage dealt by another entity.
    /// Negative values are perceived as heal.
    /// </summary>
    /// <param name="value"></param>
    public void ReceiveDamage(double value)
    {
        CurrentValue = CurrentValue - value;

        if (CurrentValue < 0)
        {
            var tag = gameObject.tag;

            Destroy(gameObject);
            if (tag == "Player")
                SceneManager.LoadScene(SceneManager.GetActiveScene().name); // Reset the game on player death.
            else
            {
                // Drop experience to the player on enemy death.
                var player = FindObjectOfType<ShinyTinyRPGManager>().GetPlayerGo();
                var expDrop = (int)ShinyStatsEntity.EvaluateStat("Experience Drop");
                player.GetComponent<ShinyStatsEntity>().AddExp(expDrop);
            }
        }
    }
}

We can also add a passive health regen that we always have in RPG games.

...
private void FixedUpdate()
{
    // Regen of the Health Points over the time.
    if (CurrentValue < MaxValue)
        CurrentValue += Time.deltaTime;
}
...

CharacterSheet UI script

A last script good to share is the character sheet panel code.

We adopted a stateless design that solves the ShinyStatsEntity reference on its own when possible. Indeed, the player can change its character at any moment, breaking references due to GameOject.Destroy() and GameObject.Instanciate() actions I am using. There are many ways to get over this, but this design is modular and maintainable.

using ShinyStatsn;
using UnityEngine;
using UnityEngine.UI;

public class CharacterSheetUI : MonoBehaviour
{
    public Text Subtitle; // Set in the Inspector.
    public Text[] AttributesValue; // Set in the Inspector.
    public Text[] StatsValue; // Set in the Inspector.
    public Text SpecialSkill; // Set in the Inspector.

    private ShinyStatsEntity ShinyStatsEntity;

    private void FixedUpdate()
    {
        if (ShinyStatsEntity == null)
        {
            var go = GameObject.FindWithTag("Player");
            if (go != null)
                ShinyStatsEntity = go.GetComponent<ShinyStatsEntity>();
        }

        if (ShinyStatsEntity == null)
            return;

        subtitle.text = ShinyStatsEntity.ClassSelected.label + " Lv. " + ShinyStatsEntity.CurrentLevel;

        AttributesValue[0].text = ShinyStatsEntity.GetAttribute("Stamina").ToString("0.#");
        AttributesValue[1].text = ShinyStatsEntity.GetAttribute("Strength").ToString("0.#");
        AttributesValue[2].text = ShinyStatsEntity.GetAttribute("Intellect").ToString("0.#");
        AttributesValue[3].text = ShinyStatsEntity.GetAttribute("Agility").ToString("0.#");

        StatsValue[0].text = ShinyStatsEntity.EvaluateStat("Health Points").ToString("0.#");
        StatsValue[1].text = ShinyStatsEntity.EvaluateStat("Mana Points").ToString("0.#");
        StatsValue[2].text = ShinyStatsEntity.EvaluateStat("Attack Power").ToString("0.#");
        StatsValue[3].text = ShinyStatsEntity.EvaluateStat("Spell Power").ToString("0.#");
        StatsValue[4].text = ShinyStatsEntity.EvaluateStat("Attack Speed").ToString("0.#");
        StatsValue[5].text = ShinyStatsEntity.EvaluateStat("Walking Speed").ToString("0.#");
    }
}

An performance improvement is possible: register to the OnLevelChanged event to catch updates instead of looping in the FixedUpdate loop.