Apr 102017
 

The World

My name is Constantine. I took it upon myself to record the reality and events of this turbulent time, as truthfully as humanly possible.

Our beautiful world consists of vast seas, here and there littered with archipelagos and islands. The largest cluster of islands has approximately 2 million square kilometers of landmass. Other than that, there are a few other smaller archipelagos or isolated islands around the world.

The main cluster is mostly dominated by rocky terrain and mountainous ranges, with limited arable land, but rich deposits of minerals and ores. Because of this set-up, most commerce flows by the sea and air, on the magnificent steamships and airships.

The society, at least the middle to upper classes living on or near Capital Island, the largest piece of land, has been thriving since the invention of the steam engine. Which brought about an industrial revolution, changing the world. In the last few decades, the world has witnessed more technological advancement than ever before in its history.

Coal is the main resource fuelling the industry — power stations, land, sea, and air transportation, manufacture, and the military. But, in the more advanced facilities, special laboratories, and top military installations, first combustion engines omen a new coming age.

Capital Island is experiencing a record growth in commerce, population, and a boom of construction industry. New buildings are popping up every year in an architectural combination of clean ornamental geometry and functional industrial evolution. The culture in these times reflects the technological advancements and admiration for the modern machine.

Progress Above All

Apart from the insignificant isolated island states, every piece of land, everything, and everyone is controlled by one central government body — the Supreme Bureau. The institution governs every inch of people’s lives — from education, production, to consumption of resources — and also directs scientific research and cultural development. Nothing functions without the approval of this bureaucratic apparatus.

“Progress above all!”

Things were not always such. The world used to be divided in many island states and local domains. But the Supreme Bureau, with its ever-present motto and disciplined military might, arose as the dominant force and subdued the world under its watchful eye. Shortly after suppressing the last widespread rebellion against the authoritative regime, top seismologists on Capital Island detected a huge earthquake 450 miles south of the Bureau’s residence, on the high seas of the Meridian Ocean.

The main cluster suffered one of the worst tsunami hits ever recorded. In the aftermath of this nearly cataclysmic event, the military brass of the Bureau thought the rebels were still operational, abusing unknown unconventional weaponry. However, investigation of the epicenter of the quake ruled out rebels’ involvement. It was in one of the most dangerous and unpredictable waters of the whole world — Mare Vaporum. The Sea of Fumes.

Mare Vaporum

No one really knows when or how it was formed. Mare Vaporum got its name from the vast amount of fumes that exudes from the water. The source of this specific behavior was unknown. The sea in these parts is unnaturally gassy and has higher temperature than the surrounding areas. The fumes obstruct navigation and make the water highly corrosive.

Due to low visibility and aggressive corrosion, both steamships and airships have long avoided this part of the world. Notorious sailors’ superstition also played a rather vital role in this, as many ships were lost without a trace in Mare Vaporum before.

Now, the Supreme Bureau decided to delve right into the mystery, sending diver teams equipped with bathyscaphes down there, into the deep. They sent out several teams to explore the underwater area. Some of the bathyscaphes never reached the seabed. Often the crew of these underwater machines had issues with failing machinery or hull-breaching. Some of them delved deeper, but disappeared without a trace, probably simply getting lost due to bad visibility caused by the fumes. No wreckage or debris of the bathyscaphes were found. As if there was something that didn’t want to be found.

But the Bureau did not give up. If natural powers, or whatever it was, were opposing them, it could only mean that something worthy must have been on the seabed of Mare Vaporum. At the end, after several failed attempts, their effort paid off! They found something…

Arx Vaporum

Something they must have found very valuable, as they did not hesitate to sacrifice human lives in retrieving the thing. Soon after, they launched new scientific projects and built research facilities on the nearest hospitable piece of land, the Katkennut Island. Grandiose projects in scale. Some of the experiments carried out didn’t end well. In fact, there are rumors speaking about catastrophic fire on the island. But the specifics were vague, newspapers only mentioning some kind of a natural disaster.

Then, a few years ago, there was another rumor among the intellectual elite of the Isles. The Supreme Bureau was building something big right in the midst of Mare Vaporum. A citadel, maybe a tower, codenamed the Arx Vaporum, dedicated to a specific scientific goal. They started to select people for the project. The selection process was very rigorous, employing elite engineers, scientists, workers, military, and other personnel. It is said they hired not only individuals, but whole families to help them build a new better world.

But what is this Arx Vaporum, and what secrets does it hold? We are not sure about this. What is known, but not generally acknowledged by the Bureau, is that they found something on the seabed that can change our future. The technological marvels of our time are only the beginning. But perhaps, there is something more sinister in all of this.

One can only wonder unless he or she enters the grounds of this mysterious tower…

— Constantine H. Darknoll, Chronicler

Mar 282017
 

Screenshot 2017-03-28 17.02.38

Hey folks!

Vaporum has just been Greenlit! It took 8 days during which we got well over 2,000 yes votes. Thank you all for your support and feedback we have received! We appreciate your time and all the comments as well.

No time for celebration yet, though! There’s still work to do before we release this beast. Namely:

  • Steam integration to enhance the game with all the platform’s goodies.
  • Finishing and enriching all the content.
  • Final touches on both audiovisuals and gameplay features.
  • And, as always, lots of playtesting!

The plan is to release Vaporum some time during the coming spring. Stay tuned for more updates!

Aug 222016
 
Example spreadsheet of Vaporum game data.

Dark theme for the win!

Let’s get straight to the point!

Spreadsheets are excellent for setting and tweaking static game data. In my experience, nothing comes even close to the speed and convenience of spreadsheets, with all the modern editing capabilities. JSON, YAML, TOML, XML, SQL, they all fade before the king!

Why

Because:

  • Baking game data into source code is outright bad! It’s typically hard to find any specific value and requires recompilation on every change.
  • XML and JSON are verbose and it’s difficult to change multiple related values quickly. Say, you need to tune down the health of all enemies. Oh well…
  • Full-blown database systems like MySQL are too heavy to set up and maintain, to my liking.

How

We’re using LibreOffice Calc (free alternative to MS Excel) to create sheets of data. All kinds of data. Enemy health, skill cooldowns, damage values, traits, attributes, you name it. The sheet is a perfect fit for this kind of tabular data — related keys of many elements. To update the game with the tweaked values, we simply export all sheets into CSV files that the game can read.

Sheets

The sheet is typically set up like this:

  • The column marked with !MainKey is reserved for identification of the element. The game creates a dictionary entry for each cell in this column. !MainKeyVert is used for vertical sheets — the game processes them with a slight difference.
  • The first row are the keys. The game reads these keys and assigns the column entries to the given main key entry.
    • Most keys are standard, meaning that they repeat many times for many different game objects / prefabs / actors.
    • Unique keys are marked with !, and the game reads them in-line along with the value. This is the best we came up with for non-tabular data, e.g. keys that never or seldom repeat. If you have a unique skill that does something that no other skill does, the key for the value of that action is most likely going to be unique. So this is for that case.
  • The second row are nice names so it’s easy to understand what is what at a glance. This is ignored by the game. It’s only presentational.

Automation

As I mentioned, we save all the sheets from the doc into CSV files, so the game can read the precious data. But, it would be horrid to manually re-export every sheet to a CSV file every time you changed a value. LibreOffice script to the rescue! It exports all the sheets to CSV files whenever we save the doc. The game then simply loads all these CSV files into various static dictionaries. Automation for the win!

Other parts (characters, skills, attribute banks…) then simply read the values from the proper dictionary by a key they get from the prefab name (or whatever suits us).

The loader class can be as simple as this:

using UnityEngine;
using UnityEngine.Assertions;
using System;
using System.Collections.Generic;

// ----------------------------------------------------------------

public class VapCsvReader
{
    /// <summary>
    /// Delimiter used for parsing CSV files.
    /// </summary>
    private const char CsvDelimiter = '\t';

    /// <summary>
    /// We ignore columns marked with this key. They are only there to improve editing experience & convenience.
    /// </summary>
    private const string PresentationKey = "-";

    /// <summary>
    /// We treat columns marked with this key as key-value pairs, parsing them as such. Used for ids.
    /// </summary>
    private const string CustomDataKey = "!";

    /// <summary>
    /// Delimiter for parsing cells that themselves contain key-value pairs.
    /// </summary>
    private const char CustomDataDelimiter = ':';

    private static bool verticalMode;

    // ----------------------------------------------------------------

    /// <summary>
    /// Reads a CSV file into a dictionary.
    /// </summary>
    /// <param name="resourcePath">Path to the CSV file.</param>
    /// <returns>A dictionary of strings and list of strings.</returns>
    public static Dictionary<string, List<string>> ReadCsv(string resourcePath)
    {
        // Get text asset.
        var text = Resources.Load(resourcePath) as TextAsset;
        Assert.IsNotNull(text, string.Format("{0} not found!", resourcePath));

        // Create dict.
        var dict = new Dictionary<string, List<string>>();

        // Get rows.
        var rows = text.text.Split(new[] { Environment.NewLine }, StringSplitOptions.None);

        // Iterate, skipping first 2 rows (1st = keys, 2nd = presentation only).
        for (var i = 2; i < rows.Length; i++)
        {
            // Get all cells of the row.
            var cells = rows[i].Split(CsvDelimiter);

            // First cell is an id.
            var id = cells[0];

            // Or it ain't. :) In that case, carry on.
            if (id == string.Empty)
                continue;

            // Create a list.
            var list = new List<string>();

            // Add it to the dict.
            dict.Add(id, list);

            // Go thru the cells, skipping the first.
            for (var n = 1; n < cells.Length; n++)
            {
                // Add a cell's content to the list.
                list.Add(cells[n]);
            }
        }

        return dict;
    }

    // ----------------------------------------------------------------

    /// <summary>
    /// Includes data loaded from a CSV file into the given Dictionary.
    /// </summary>
    /// <param name="resourcePath">Path to the CSV file.</param>
    /// <param name="dict">The Dictionary to include into.</param>
    public static void IncludeCsvIntoDict(string resourcePath, Dictionary<string, object> dict)
    {
        // Set default mode.
        verticalMode = false;

        // Get text asset.
        var text = Resources.Load(resourcePath) as TextAsset;

        // Get rows.
        var rows = text.text.Split(new[] { Environment.NewLine }, StringSplitOptions.None);

        // The very first row is reserved for keys, so read that.
        var keys = rows[0].Split(CsvDelimiter);

        // Find the key which we'll use as the base for inclusion.
        var baseKeyIndex = Array.IndexOf(keys, "!MainKey");

        if (baseKeyIndex == -1)
        {
            baseKeyIndex = Array.IndexOf(keys, "!MainKeyVert");

            Assert.IsTrue(baseKeyIndex != -1, string.Format("{0} is missing a !MainKey or !MainKeyVert entry!", resourcePath));

            verticalMode = true;
        }

        // Iterate, skipping first 2 rows (1st = keys, 2nd = presentation only).
        for (var i = 2; i < rows.Length; i++)
        {
            // Get a row.
            var r = rows[i];

            // Get all cells of the row.
            var cells = r.Split(CsvDelimiter);

            // Determine the base key.
            var baseKey = cells[baseKeyIndex];

            // If the cell with the base key index is empty, nothing to do here. Skip the row.
            if (baseKey == string.Empty)
                continue;

            // Go thru keys.
            for (var n = 0; n < keys.Length; n++)
            {
                // Get a key.
                var key = keys[n];

                // Skip presentational keys and the base key index. Got nuttin' to do with those.
                if (key == PresentationKey || n == baseKeyIndex)
                    continue;

                // Get the cell content with the key's index.
                var cell = cells[n];

                // Empty cells don't wanna be touched. Skip 'em.
                if (cell == string.Empty)
                    continue;

                // If the key tells us to parse the cell as a key-value pair...
                if (key == CustomDataKey)
                {
                    Assert.IsTrue(cell.Contains(CustomDataDelimiter.ToString()), string.Format("{0} error in file {1}: Custom data must have '{2}' as key-value delimiter at row {3}, column {4}", typeof(VapCsvReader), resourcePath, CustomDataDelimiter, i + 1, n + 1));

                    // Split the cell by the custom data delimiter.
                    var data = cell.Split(CustomDataDelimiter);

                    // Set the key to the left side.
                    key = data[0].Trim();

                    // Set the cell content to the right side.
                    cell = data[1].Trim();
                }

                // Construct our final full key, so longed for for countless ages across many continents.
                var fullKey = baseKey + "." + key;

                // These verticals gotta be special, hm?
                if (verticalMode)
                {
                    fullKey = key + "." + baseKey;
                }

                // Bitch the fuck out of it when trying to add keys that're already there!
                Assert.IsTrue(!dict.ContainsKey(fullKey), string.Format("What the hell is wrong witchu' son? Somebody already slapped key '{0}' onto the dictionary! So either you or them is pretty misled, dude.", fullKey));

                // Add the key.
                dict.Add(fullKey, cell);

                // Fix any missing keys in the dictionary along the path to the full key. This is for other classes to be able to ask whether some key even exists before doing more work.
                // IMPORTANT: Me loves 'while trues'!
                while (true)
                {
                    // Get the index of the dot.
                    var dotidx = fullKey.LastIndexOf('.');

                    // If there ain't any, get the horse outta here.
                    if (dotidx == -1)
                        break;

                    // Chop the full key up to the last dot (exclusive).
                    fullKey = fullKey.Substring(0, dotidx);

                    // Now add it to the dict if it ain't there already.
                    if (!dict.ContainsKey(fullKey))
                    {
                        dict.Add(fullKey, null);
                    }

                    // And now, the bracket slide...
                }
            }
        }
    }
}

If you don’t feel like writing your own or using ours, there are good libs out there:

We also have a neat class to manage the static dictionaries, to help retrieve values from it, but I’ll leave that to you to come up with. Don’t wanna get spoiled too hard, right? 😉

Unique keys go into the cell as a key-value pair.

Unique keys go into the cell as a key-value pair.

Conclusion

We were using our own, simplified implementation of YAML for all game data, but that became a chore to maintain. The number of elements just grew too much.

Once we created this sheet data flow, it’s become a breeze to not only tweak, but also find and understand all the values at a glance. Shorter iteration times (tweak -> test) help a ton for any project bigger than Tetris! And besides, attentive readers noticed we had three ‘for the wins’ in the article, so it has to be cool! 🙂

May 262016
 

dice picture in vaporum article about randomness

RNG stands for random number generator. In this article, I’ll show you what kind of RNG we use in various chance-based actions in Vaporum.

Why

One of the greatest RNG evils in games is long streaks of success or failure. Especially failure! How we hate to miss the attack 4 times in a row while the hit chance is 80%!

But why even care? We can just use this and be fine, right?

if (UnityEngine.Random.value < myChance) Hit();

Hell no! This most basic approach has exactly the one glaring issue: big streaks of luck or unluck (what a word). You will get them on regular basis. They are bad for your game.

Let’s see what we can do about them pesky streaks.

Where

First, let’s see where we use RNG. Major chance-based actions in Vaporum are:

  • Weapon attacks (enemy attacks too)
  • Critical hits
  • Various on-hit passive skills and traits

To ensure consistent results of various actions, we have an instance of RNG on each of them. Every time an action is checked for success, based on the given chance, the RNG is polled for a result. It either returns true or false, success or failure.

Each weapon (enemy attacks count as weapons too) has two instances of RNG. One for hit chance, one for crit chance. Whenever you attack with a weapon, we check the hit RNG. If we got a hit, we then check the crit RNG.

Other effects, mostly passive traits, are only checked when appropriate. For example, if you have a passive trait that adds a chance to stun the enemy with hammers, and you attack with a hammer, the RNG of the trait is checked. If it succeeds, we apply the stun.

How

After trying out 3 various RNG systems (designed by us), we decided for the pseudo-random distribution (PRD) model. It gives us consistency of results, almost exclusively eliminates long unprobable streaks, but still leaves some room for occasional short streaks. You can read a fine article about it on this Dota 2 fan page.

The basic premise is that whenever your success check fails, the chance for the next check increases, and this goes on and on until it succeeds. And when it does, the chance is reset back to the base value. The base chance value is not the desired chance (as shown in tooltips for weapons and skills), but a pre-calculated constant. The first check is also run against this constant, not the displayed chance. Read that article to get a gist of it if you haven’t already.

Now the problem is, it’s hard to calculate that constant. As you can see, they have a pre-calculated constant for every 5% up to 80% in Dota 2. That’s fine for Dota, but not for us. We need constants for each percent, from 0% to 100%.

So how we get them? Approximation comes to help!

(It is a console application written in VS2015. Add a reference to System.Windows.Forms in your project (right-click project -> add reference -> ..., profit)).

using System;
using System.Diagnostics;
using System.Globalization;
using System.Windows.Forms;

namespace PRDTableGenerator
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            // Run the beast.
            PRDPercentageTableGenerator.Generate();

            // Wait for input so the console doesn't vanish into thin air.
            Console.Read();
        }
    }

    public static class PRDPercentageTableGenerator
    {
        // How many cycles to go thru when approximating. The greater the number, the more precise the result, but also greater generation duration.
        private const int statisticCycles = 500000;

        // The maximum allowed deviation of approximated probability from desired probability. Greater equals more precise and time-consuming.
        private const double maxDeviation = 0.00001;

        // Log messages about how we're faring?
        private static bool logEnabled = true;

        // Our basic RNG.
        private static Random random = new Random();

        // ------------------------------------------------------------

        private static void Log(string message, params object[] args)
        {
            if (!logEnabled) return;

            Console.WriteLine(message, args);
        }

        // ------------------------------------------------------------

        // Check if we succeed with given chance.
        private static bool Roll(double chance)
        {
            return random.NextDouble() < chance;
        }

        // ------------------------------------------------------------

        // Test whether the given approximated C value results in a probability close enough to the desired probability.
        private static int TestC(double baseVal, double desiredChance)
        {
            var hits = 0;
            var c = baseVal;

            for (var i = 0; i < statisticCycles; i++)
            {
                if (Roll(c))
                {
                    hits++;
                    c = baseVal;
                }
                else
                {
                    c += baseVal;
                }
            }

            // Approximated probability.
            var avg = (double)hits / (double)statisticCycles;

            // Deviation from desired probability.
            var diff = avg - desiredChance;

            // Close enough.
            if (Math.Abs(diff) < maxDeviation)
            {
                return 0;
            }

            // Nope! Too little.
            else if (diff < 0)
            {
                return 1;
            }

            // Nope! Too much.
            else
            {
                return -1;
            }
        }

        // ------------------------------------------------------------

        // Get an approximated C for the given probability.
        private static double ApproximateCValue(double chance)
        {
            // Current C we're trying to get right.
            var triedC = chance;

            // Modifier by which to approximate on failed attempts.
            var mod = triedC * 0.5f;

            while (true)
            {
                // Check if we're close.
                var result = TestC(triedC, chance);

                // Ya bet we are, gringo!
                if (result == 0)
                {
                    Log("Approximated C value: {0}", triedC);
                    return triedC;
                }

                // Nah, we shot too low. Need to up the game.
                else if (result == 1)
                {
                    triedC += mod;
                }

                // Damn it, we overshot. Need to calm it down a lil.
                else
                {
                    triedC -= mod;
                }

                // Decrease the modifier so we're gonna get close enogh eventually.
                mod *= 0.5f;
            }
        }

        // ------------------------------------------------------------

        private static string GenerateCSharpArray(float[] t)
        {
            var s = string.Empty;

            s += "private static float[] chanceTable = new float[101]\n{\n";

            for (var i = 0; i < t.Length; i++)
            {
                s += string.Format("\t/* {0}% */ {1}f,\n", i, t[i]);
            }

            s += "};";

            return s;
        }

        // ------------------------------------------------------------

        // Go go go, soldier!
        public static void Generate()
        {
            // Make sure we use dots as decimal separator.
            var customCulture = (CultureInfo)System.Threading.Thread.CurrentThread.CurrentCulture.Clone();
            customCulture.NumberFormat.NumberDecimalSeparator = ".";
            System.Threading.Thread.CurrentThread.CurrentCulture = customCulture;

            // Let's measure how long it takes.
            var sw = new Stopwatch();
            sw.Start();

            Log("Generating...");

            // Create the array and fill the boundaries with the known values.
            var t = new float[101];
            t[0] = 0.0f;
            t[100] = 1.0f;

            // Toil away! Assign an approximated C value to each percentage probability in the array.
            for (var i = 1; i < t.Length - 1; i++)
            {
                Log("\nGenerating C value for {0}% chance...", i);
                t[i] = (float)ApproximateCValue(i * 0.01);
            }

            // Write the result, nice and dandy.
            Console.WriteLine("\nResulting array of floats:\n");

            var res = GenerateCSharpArray(t);
            Console.WriteLine(res);
            Clipboard.SetText(res);
            Console.WriteLine("\n(Array copied to clipboard.)");

            Console.WriteLine("\nTo understand how to use pseudo-random distribution in your game, check this cool article: http://dota2.gamepedia.com/Pseudo-random_distribution");

            // Write the duration.
            Console.WriteLine("\nGeneration took: {0:F1} seconds", sw.Elapsed.TotalSeconds);
            sw.Stop();
        }
    }
}

Note: Running this takes about 30 seconds on my computer. May vary depending on your rig.

Whoa, so we get a static table now! Here’s how to use it:

using UnityEngine;

public class VapPRDRandom
{
    private static float[] s_chanceTable = new float[101]
    {
        /* 0% */ 0f,
        /* 1% */ 0.0001611328f,
        /* 2% */ 0.0006103492f,
        /* 3% */ 0.001390287f,
        /* 4% */ 0.002450562f,
        /* 5% */ 0.003812454f,
        /* 6% */ 0.00544064f,
        /* 7% */ 0.007358261f,
        /* 8% */ 0.009589888f,
        /* 9% */ 0.01202478f,
        /* 10% */ 0.01469073f,
        /* 11% */ 0.01783203f,
        /* 12% */ 0.02094635f,
        /* 13% */ 0.02451059f,
        /* 14% */ 0.02819672f,
        /* 15% */ 0.03222107f,
        /* 16% */ 0.03644421f,
        /* 17% */ 0.04079834f,
        /* 18% */ 0.04570862f,
        /* 19% */ 0.05053855f,
        /* 20% */ 0.05546875f,
        /* 21% */ 0.0610963f,
        /* 22% */ 0.06668884f,
        /* 23% */ 0.07258219f,
        /* 24% */ 0.07850874f,
        /* 25% */ 0.08446875f,
        /* 26% */ 0.09119834f,
        /* 27% */ 0.09778266f,
        /* 28% */ 0.1045927f,
        /* 29% */ 0.111832f,
        /* 30% */ 0.1189695f,
        /* 31% */ 0.1262213f,
        /* 32% */ 0.1338988f,
        /* 33% */ 0.1418493f,
        /* 34% */ 0.1500781f,
        /* 35% */ 0.1580311f,
        /* 36% */ 0.1662726f,
        /* 37% */ 0.1752441f,
        /* 38% */ 0.1833201f,
        /* 39% */ 0.1924883f,
        /* 40% */ 0.2017426f,
        /* 41% */ 0.2110553f,
        /* 42% */ 0.220342f,
        /* 43% */ 0.2302755f,
        /* 44% */ 0.2397724f,
        /* 45% */ 0.2494584f,
        /* 46% */ 0.2596484f,
        /* 47% */ 0.27014f,
        /* 48% */ 0.280897f,
        /* 49% */ 0.29093f,
        /* 50% */ 0.3019776f,
        /* 51% */ 0.3127797f,
        /* 52% */ 0.322937f,
        /* 53% */ 0.3341516f,
        /* 54% */ 0.3480469f,
        /* 55% */ 0.360863f,
        /* 56% */ 0.3735327f,
        /* 57% */ 0.3854644f,
        /* 58% */ 0.3983658f,
        /* 59% */ 0.4104217f,
        /* 60% */ 0.4230034f,
        /* 61% */ 0.4348656f,
        /* 62% */ 0.4462058f,
        /* 63% */ 0.4577338f,
        /* 64% */ 0.4695821f,
        /* 65% */ 0.4809301f,
        /* 66% */ 0.4931479f,
        /* 67% */ 0.5079261f,
        /* 68% */ 0.5298079f,
        /* 69% */ 0.5507703f,
        /* 70% */ 0.5715531f,
        /* 71% */ 0.5903955f,
        /* 72% */ 0.611093f,
        /* 73% */ 0.6303659f,
        /* 74% */ 0.649352f,
        /* 75% */ 0.6665279f,
        /* 76% */ 0.6846761f,
        /* 77% */ 0.7016721f,
        /* 78% */ 0.7180756f,
        /* 79% */ 0.7338443f,
        /* 80% */ 0.7496008f,
        /* 81% */ 0.7645162f,
        /* 82% */ 0.7801579f,
        /* 83% */ 0.7954523f,
        /* 84% */ 0.8106738f,
        /* 85% */ 0.8250977f,
        /* 86% */ 0.8370593f,
        /* 87% */ 0.8508409f,
        /* 88% */ 0.8628125f,
        /* 89% */ 0.8759527f,
        /* 90% */ 0.8885157f,
        /* 91% */ 0.9015872f,
        /* 92% */ 0.9131664f,
        /* 93% */ 0.9247211f,
        /* 94% */ 0.9358692f,
        /* 95% */ 0.9473711f,
        /* 96% */ 0.9582562f,
        /* 97% */ 0.9691508f,
        /* 98% */ 0.9795209f,
        /* 99% */ 0.9899905f,
        /* 100% */ 1f,
    };

    [VapSave]
    private readonly float[] m_accums = new float[101];

    // ------------------------------------------------------------

    public VapPRDRandom()
    {
        this.Init();
    }

    // ------------------------------------------------------------

    private void Init()
    {
        for (var i = 0; i < this.m_accums.Length; i++)
        {
            this.m_accums[i] = this.GetC(i);
        }
    }

    // ------------------------------------------------------------

    private bool Check(float chance)
    {
        return UnityEngine.Random.value < chance;
    }

    // ------------------------------------------------------------

    private float GetC(int chance)
    {
        return s_chanceTable[chance];
    }

    // ------------------------------------------------------------

    private void ResetC(int chance)
    {
        this.m_accums[chance] = this.GetC(chance);
    }

    // ------------------------------------------------------------

    public bool Success(float chance)
    {
        var c = Mathf.RoundToInt((chance * 100.0f));

        if (Check(this.m_accums[c]))
        {
            this.ResetC(c);
            return true;
        }

        this.m_accums[c] += this.GetC(c);

        return false;
    }

    // ------------------------------------------------------------

    public static void Test()
    {
        var s = new VapPRDRandom();

        var realChance = 0.0f;
        var realTries = 1;
        var hitStreak = 0;
        var missStreak = 0;
        var subsequentHits = 0;
        var subsequentMisses = 0;

        for (var x = 0; x < realTries; x++)
        {
            var chance = 0.9f;
            var tries = 100000;
            var hits = 0;

            for (var i = 0; i < tries; i++)
            {
                if (s.Success(chance))
                {
                    hits++;

                    subsequentHits++;
                    hitStreak = (subsequentHits >= hitStreak) ? subsequentHits : hitStreak;

                    subsequentMisses = 0;
                }
                else
                {
                    subsequentMisses++;
                    missStreak = (subsequentMisses >= missStreak) ? subsequentMisses : missStreak;

                    subsequentHits = 0;
                }
            }

            Debug.Log("\n\n");
            Debug.LogFormat("Chance: {0:P0}", chance);
            Debug.LogFormat("Tries: {0}", tries);
            Debug.LogFormat("Hits: {0}", hits);
            Debug.LogFormat("Misses: {0}", tries - hits);
            Debug.LogFormat("Hit Streak: {0}", hitStreak);
            Debug.LogFormat("Miss Streak: {0}", missStreak);

            realChance += (float)hits / tries;
        }

        Debug.LogFormat("Real Chance: {0:P1}", realChance / realTries);
    }
}

Note: The [VapSave] attribute is our fancy save system. You basically just mark anything you want to save and you’re done! Lots of work behind the curtain though! (More on that in a later post, hopefully.)

So, we just pasted the generated constants into a static array, and the rest is fairly straightforward I hope.

You can use the static Test function to test if it gives expected results for various chance percentages. However, that is just a guide. Only playing your game will show if it survives the battle-test!

Now, we need to create instances of the VapPRDRandom in classes that use chance-based actions, and just poll the Success method.

random number generator in vaporum

Conclusion

There are several ways to achieve ‘good’, consistent randomness without the dreaded streaks. For us, the pseudo-random distribution model works like a charm, solving the random issues we struggled with. Hope this helps someone out there, struggling with the same thing.

Already using a system that works for you? Do tell!

May 112016
 

Hey guys!

photo of fatbot games team on game access 2016 in brno

Game Access ’16 in Brno

Vaporum has gathered unprecedented steam in recent weeks, in terms of exposure at gaming events. As we stated in a brief post, we were going to present our game at Game Access 2016 in the lovely city of Brno.

That was one successful, strenuous, and inspiring indie event! We received overwhelmingly positive response from both journalists and the public, as well as loads of spot-on feedback on how the game plays, what it lacks, and what it excels at.

photos of vaporum at expo reboot develop 2016 in split

Reboot Develop ’16 in Split

But before we could even process all the goodness, we decided to go to Croatia for another gig — Reboot Develop 2016. With names like John Romero, Cliff Bleszinski, and Brian Fargo to give speeches, we couldn’t resist this big indie event!

Didn’t take long before people swarmed to our ‘booth’, bombarding us with countless questions while trying to beat the demo levels we’d prepared.

Atmosphere, graphics, and combat were the most praised features by people at both events. That makes us really happy, because that is exactly what we wanted to be the strengths of Vaporum from the beginning.

We’ll share details about how we fared at each event, what we’ve learned to do, not to do, and the pros / cons of presenting at such events in later posts.

May 062016
 

Hey folks!

Here’s a new video to show the current visuals of the game, which we started working on when going full-time on August 2015.

The biggest change in graphics we did since prototype was switching to PBR (physical-based rendering). We are using PBR Alloy shaders for Unity done by RUST Ltd. The shader pack works very well together with the pair of texturing tools — Substance Designer and Substance Painter from Allegorithmic. These allow us a fast and robust way to create textured assets that share the same basic materials from our own library.

3ds Max, Maya and Zbrush are the usual suspects we use to create models. Since full-time development started, we have also raised the polycount on our assets. This way we can achieve a higher visual fidelity and also give the game a more “eye-candy” feeling. For which we received awesome journalist and public feedback at gaming events we recently attended. (We’ll post about the events later this century. 😉 )

Hope you like the video, and feel free to voice your opinion on our media channels.