Shiny Tiny RPG - Game Dev using Shiny Stats

Intro

Shiny Stats, my Unity asset for stats and leveling mechanisms, is nearly done after two years of on-and-off work. What’s left is documentation and… marketing! And for an asset like this one, the best marketing material is a game that uses it. Screenshots full of data charts, which is what I had planned back in 2017, sell the mathematics. Shiny Stats isn’t about mathematics — it’s about sparing game developers the weeks I spent implementing all of this from scratch for Caatacombb.

So: a mini RPG, because that genre exercises every feature. It ships inside the asset, and that sets the brief.

Scope

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

Realm of the Mad God
  • 30 hours of work maximum, and fun to make
  • Covers every Shiny Stats feature, and is actually interesting to play
  • No external dependencies, few binaries, small enough to ship with the asset
  • Naive, beginner-friendly, unoptimized scripts — they double as documentation
  • Runs and plays in WebGL

We will call the game Shiny Tiny RPG.

Shiny Stats cover art

Game design

The vocabulary is cherry-picked from World of Warcraft, one of the most played MMORPGs ever: four attributes (Stamina, Strength, Intellect, Agility), five classes (Warrior, Paladin, Rogue, Wizard, Cleric), and six stats derived from them. Levels run from 1 to 20, on the built-in Experience System of Shiny Stats.

StatAffected by (Attribute)
Health PointsStamina
Mana PointsIntellect
Attack PowerStrength
Spell PowerIntellect
Attack SpeedAgility
Walking SpeedAgility
Stats-Attributes interactions
ClassHealth 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

A small map with some enemies to defeat and one ultimate boss. The player climbs levels by killing enemies to get strong enough to defeat the boss. Enemies are scattered from level 1 to 19, the boss is level 20.

The player picks a class at the start and can change it whenever they want, without losing their progression — not typical, but convenient for a demo. Movement is on the keyboard arrows (WASD). A left click is a regular hit, using the attack power stat; a right click is the class special skill, which costs mana.

ClassSkillDescription
AllAttackRegular attack using Attack Power
WarriorFatal Strike150% regular attack damage
PaladinHoly Strike100% regular attack damage, the damage dealt is returned as Health Points
RogueBackstab75% chance to deal 100% regular attack damage, 25% chance to deal 500% regular attack damage
WizardFlame StrikeUse Spell Power instead of Attack Power to deal damage
ClericSelf HealUse Spell Power to heal itself, no damage dealt to enemies

Time to dev

Map

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

Floor material
Wall material

To create the map, we only use Cube meshes, scaled against a 1x1x1 reference cube. The map is quite big, to put a lot of empty space between the enemies, with walls to limit the zone and guide the player. It looks like a closed dungeon, in a cheap way.

Map enemies placement
Map design with enemies placement
Difficulty to defeatEnemy lvl 1-4Enemy lvl 5-9Enemy lvl 10-14Enemy lvl 15-19Boss (lvl 20)
Player level 1-4mediumhard---
Player level 5-9easymediumhard--
Player level 10-14-easymediumhard-
Player level 15-19--easymediumhard
Player level 20---easymedium

Characters and animation

Same approach as the map: primitives mixed together instead of ‘real’ 3D models. Six characters — five playable classes, for the player and the enemies, plus one boss.

Character modeling
Character modeling using 1 sphere & 2 cylinders
Characters

Two attack animations per class, recorded with the standard Unity animation system and driven by a state machine animator. Dead easy, but far from producing awesome motions.

Shiny Stats setup

A new empty GameObject with the ShinyStats script attached, configured with the settings above.

Unity Shiny Stats GameObject
Shiny Stats configured

Then the ShinyStatsEntity script goes on the six characters, with their respective classes — the [+ - +++] range converted into real values.

ShinyStatsEntity setup
ClassStaminaStrengthIntellectAgilitySum
Warrior10155737
Paladin1569737
Rogue5891537
Wizard7518737
Cleric12513737

Stats configuration

Experience and Experience Drop are the easy ones: arbitrary values per level, tuned so a single kill takes you from 1 to 2 and about ten are needed to go from 19 to 20.

Level(s)Experience Drop (on enemy’s death)Experience (required to level up the player)Expected kills to level up
1-480 + Level x 101001 - 2 enemies
5-980 + Level x 102502 - 4 enemies
10-1480 + Level x 105003 - 6 enemies
15-1880 + Level x 1010005 - 6 enemies
1980 + Level x 10200010 - 11 enemies
201500 (the boss)--

The other six are functions of the attributes and the level, and they fall into three shapes. Walking speed is nearly flat over the levels and dominated by Agility, so a Rogue enemy outruns any player who didn’t pick Rogue. Health and mana are linear, tracking the level curve. Attack and spell power are logarithmic, so damage falls behind health in the top levels and the end-game combats last longer. Attack speed is a constant: the minimum delay in seconds between two attacks. Varying it would have fought with the attack animations.

StatFormula
Walking Speed20+Agility+Level/1020 + Agility + Level / 10
Health Points20+Stamina+Stamina×(Level−1)/420 + Stamina + Stamina \times (Level - 1) / 4
Mana PointsIntellect+Intellect×(Level−1)/2Intellect + Intellect \times (Level - 1) / 2
Attack PowerStrength/5+Strength×log(Level,10)Strength / 5 + Strength \times log(Level, 10)
Spell PowerIntellect/5+Intellect×log(Level,10)Intellect / 5 + Intellect \times log(Level, 10)
Attack Speed0.20.2
Rogue attack power chart

Walking speed for Rogue (this class has more agility) versus Wizard:

Shiny Stats preview for the Rogue class

UIs

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

UI panels state machine

Code

The script worth sharing is the health system, because it is where the game actually talks to Shiny Stats: it subscribes to OnLevelChanged, reads Health Points through EvaluateStat, and hands out experience with AddExp when an enemy dies.

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);
            }
        }
    }
}

Plus the passive health regen we always have in RPG games.

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

Final result

Try the demo (from the Shiny Stats website)