12 January 2020 game dev Unity C# Shiny Stats
Shiny Tiny RPG - Game Dev using Shiny Stats
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 (usingattack power
stat). Trigger with mouse LEFT clickspecial 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