Intro

The development steps for Shiny Stats, my Unity Asset product, are nearly over.
After roughly 2 years of work, quite not regular though, there are still plenty of things to do such as documentation and… marketing !

Marketing today will be about making a game demonstrator to show off Shiny Stats awesomeness. Let’s go.

Demo game

Having a playable demo to convince developers to adopt (to buy!) Shiny Stats will definitively help. The older-version-of-me didn’t see this coming back in 2017 when I did the initial planning. I told myself that screenshots with a lot of data charts would do the job as communication material. This was very pragmatic, but I realize today that this will not advertise Shiny Stats the right way. Shiny Stats is not about mathematics, it’s about improving game developers productivity so they can focus on what matters.

Shiny Stats aims to give game makers a ready-to-use utility to add stats and leveling mechanisms into their games. It is a wide subject, a complex one, and a lot of game uses these mechanisms. This is the kind of asset I would have bought while making Caatacombb in 2015 because this was very long to implement from scratch.

My demo should showcase a mini RPG example because this genre covers all the Shiny Stats possibilities thus this is my key audience I want to focus on. And I’ll have to be careful with the communication to make it clear that the Unity Asset also fits for non-RPG games, such as Tower Defense games for instance.

This game demo will be shipped with the Shiny Stats Unity Asset and I don’t want to bother game developers with a heavy demo. So I won’t reuse any official Unity demo code and assets. Unity’s demos are also a bit over complex for my need. That is exactly why I can’t also reuse my Caatacombb game assets either! So we’ll kinda make the game from scratch.

Making a Game

Name it

Ok, we are about to make a game here. Almost there. First, let’s pick a name for this demo-game: Shiny Tiny RPG.
That’s cute, it reminds Shiny Stats, and has the RPG tag in it, this will be our tiny RPG game featuring Shiny Stats.

Game Design

Minimalist as fuck. A bit like Realm of the Mad God but with 3D tech.

We will follow that quote for the whole game conception.


Realm of the Mad God

Few good things to remember:

  • I am willing to spend 30 hours of work max
    • And have fun making it
  • The game demo
    • Cover all Shiny Stats features
    • Is interesting to play
    • Reusable for marketing & documentation

Some Technical requirements:

  • As small as possible since it will be shipped with Shiny Stats
    • No external dependencies
    • Reduced use of binaries (Will I need textures?)
  • Scripts code must be naïve, beginner-friendly and not optimized to enhance readability
  • Running & playable in WebGL

Shiny Stats configuration

Wooh, we start the game conception with the Shiny Stats config.
I am cherry picking a bit into World Of Warcraft’s vocabulary since it was one of the most played MMORPG game ever.

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

We will set up a small map where live some enemies to defeat and one ultimate boss. The game idea is that the player has to climb levels by killing enemies to improve its power in order to be strong enough to defeat the boss.
The enemies will be scattered on the map, from level 1 to 19. The boss will be level 20.

The player starts the game by choosing his class. He can change his class whenever he wants, without loosing his progression - Not typical, but this is convenient for the demo purpose.

Character movements will be with the keyboard arrows (WASD).

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

Time to dev

Map

I’ve setup two materials for the environment, one for the floor and one for the walls.

To create the map, we only use Cube meshes. We use a 1x1x1 cube as a reference to scale the map properly. In our need, the map will be quite big to put a lot of empty space between the enemies, with walls to limit the zone and guide the player. It will look like a closed dungeon, in a cheap way.

That’s already during this map conception that we need to define the enemies’ level distribution inside the map and the inner combat challenge. So we need to define the meta now, at least on paper.

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


Map conception with enemies placement

Characters

Similar to the map, we will only use primary meshes mixed together instead of doing ‘real’ 3D models.


Character modeling using 1 sphere & 2 cylinders

We create 6 characters: 5 playable classes (player and enemies) and one boss.

Animation

We create a set of 2 attack animations for each class using the standard Unity animation system with an animator controller, which allows us to record keyframes on the go and use a state machine animator for the logic.
Dead easy, but far from producing awesome motions.

Shiny Stats

We will quickly setup the stats mechanisms thanks to Shiny Stats.

I create a new empty GameObject, and attach the ShinyStats script on it. Let’s configure it with setting we have specified so far.


Then, I attach the ShinyStatsEntity script to the 6 characters and setup their respective classes.

Class configuration

I convert the [+ - +++] range we’ve designed before into real values.

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

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.

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

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\)


Shiny Stats preview for the Rogue class

Shiny Stats preview for the Wizard class

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.

UIs

With a reduced amount of UI panels, we can keep the game really tiny and still showing off a lot of stats… from Shiny Stats.


UI panels state machine

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.

Final result

Try the demo (from the Shiny Stats website)


Character Sheet panel


Class Selection panel


Game panel