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.

Mar 172016
 

Hey guys! Just a quick update on what we’ve been up to lately.

We did a focus test where we invited various people to come to play our combat test, most of them never having played the game before. We needed to see if we’re on the right track with the real-time combat, melee vs ranged, and various other features in it. Receiving basically universal praise from the testers boosted our confidence in that we had made the right decisions. Of course, the guys also pointed out some flaws, which we quickly fixed after the test was over.

AssaulGuardian

We have about half of all planned enemies in the game, fully functional, running around doing nasty things (mostly to the player). The focus test proved we already have a solid bunch of bad guys where each has different strengths and weaknesses, poses different types of threats, and requires a different approach to beat (lots of differents, eh?).

LevelDesign

All basic building bricks like trapdoors, pushable crates, destructibles, levers, teleports, and such are done. We also have a solid design on all the puzzles and game situations, which we build out of these basic bricks.

Inventory, items, and basic RPG system are also fully functional. We have several levels designed on paper, with major puzzles, situations, and storytelling. Our custom level editor allows us to whip up all the corridors, halls, and rooms in no time, so that’s gonna be a blast!

Right now, we are:

  • Enriching our RPG system with all sorts of attributes, skills, talents, traits, and whatnot.
  • Building up GUI so you can play with all the RPG goodies with ease.
  • Adding more enemy types.
  • Adding wallsets and architectural gems.
  • Designing more levels and ways to screw the player. 😉
  • Designing boss encounters, plus the final big boss showdown.

We don’t post often, but rest assured we use all the time to work on the game.

Discuss in our reddit or other social media, share, and have a nice one!

Nov 252015
 

Hi!

Sorry to be quiet for so long. We’ve decided to post only about highlights or milestones rather than churning out insignificant posts often. Saved time will always come in handy! Nevertheless, we’ve been working hard on various game systems, levels, and some goodies too.

One of which is the Flamer Guardian. This badass giant of a man has few weaknesses, but a lot of firepower. As with most of our enemies, you will need to devise a tactic to beat him rather than just banging at his thick armor head on.

FlamerGuardian_01

The Guardian is based on the concept art by Roman Mindek, who is currently working full-time on Kingdom Come: Deliverance. Check his portfolio here!

The big guy is modelled, rigged & animated by our in-house character artist Lukas Chrapek (who is also a big guy 😉 ). Lukas is in charge of the technical side of graphics, besides his bread-and-butter work of character-modelling, rigging, and animating. He previously worked on various projects as a freelancer. Before joining the Vaporum team, he worked at Keen Software House as full-time lead artist on Space Engineers and Medieval Engineers. You can check out his portfolio here.

We have moved to PBR workflow in our project, and Lukas made a damn good use of it on the Guardian. Take a look at the Flamer Guardian in all his glory in Marmoset Viewer here.

Feel free to comment and share on our subreddit, Facebook page, and Twitter.

Aug 172015
 
Vaporum title concept art

Gigantic and scary, but strangely familiar

In today’s post, we’re going to have a deeper overview at what Vaporum really is, plus a recap of what you’ve might already seen.

Overview

Vaporum is an action-based dungeon crawler set in an original steampunk world. There is no party creation; you only control one character, and that character is deeply involved in a mysterious storyline. You move on a grid of squares, and everything happens in real time: exploration, puzzle-solving, combat. The game uses randomization in certain aspects to make every play-through a fresh experience. There might be some nasty surprises waiting for you in the dark corners of the complex, too. Immersion is one of the major priorities for us.

Setting and story

The game is set in a dystopian world where almost everything is controlled by a single governmental body — the Great Administration. As steam machinery is slowly being replaced by more modern diesel-powered achievements of science, something new looms over the horizon.

Our hero wakes up in the middle of a sea, weak and battered, with only a faint trail of memory. The only thing that’s plain and concrete in this sea of vapors is a megalithic tower-like structure, standing taller than clouds.

With nowhere else to go, the hero delves into the unknown…

Single character

At the game start, you do not create a party of adventurers. Instead, you control a single character. A character with a personality. Having just one character better suits Vaporum’s action-based nature, as well as his deep story involvement.

This also means that this one character must have plenty of options to choose from when striving for the optimal build.

Classless

There are no classes. You can build your character any way you want, starting up with a clean sheet. And boy, there are options to choose from!

To be precise, you don’t build your character; you rather improve your suit! As you gain fumium, the mysterious substance found in dead enemies and tubes across the complex, your suit becomes more powerful, unlocking new possibilities and bonuses for you to choose from. You often have to make an irrefutable choice between two possible upgrades which tend to contradict each other, leading to very different gameplay implications. This makes your every choice a hard and important one!

Then there are gadgets! These are items you can install into your suit which grant unique skills. You can replace gadgets with newer or even different ones, to change your play-style on the spot. Most gadgets can also be improved, again with a one-time choice between two powerful upgrades.

This fluid set of skills with the more static build of your suit make up for a rather unique system.

We think that tweaking a suit better suits the steampunk setting (pun intended 😉 ). We will have a detailed post dedicated to this topic later on.

Movement

In terms of movement, we follow the genre-traditional scheme of grid-based movement on squares. This allows us to create puzzles and gameplay situations that would be nearly impossible in a free movement system. We chose this movement type deliberately, as we like the old-school dungeoneering vibe!

Exploration

Exploring the complex, you will uncover bits of story, nasty enemies, and mind-bending puzzles to test your mettle. To battle the odds, you will find items, gadgets, and various consumables. There are optional areas you will be able to explore to gain more fumium and powerful items, at a price of difficult challenges.

Combat

As everything runs in real time, including combat, the outcome of a fight depends on both your build and personal skills. We strive for a middle ground between tactical number-crunching and pure action — you will need both!

Each enemy type has a distinct set of strengths and weaknesses, and a set of attacks and defensive mechanisms. While some enemies might be dumb, following you to any danger, some others are smart and difficult to beat.

On the other hand, we’re not overdoing the AI to be as sapient as a chimp! While some games proudly claim to have imba-smart AI, we sure aren’t going that route. We believe AI should be fun to play against, not frustrating-clever or unchallenging-dumb.

Randomization

Vaporum uses randomization for placing items throughout the complex, as well as what specific items spawn. This means that you will never find the same items at the same spots on two play-throughs.

We’re also tinkering with the idea of slight map randomization where the maps are always the same, but some paths might be blocked while others open, depending on the random factor. This would also make the game a little different every time you play.

Machinery

You will come across various steam, diesel, and fumium-powered machinery that usually needs a component or two to work. These present main game puzzles you must solve to progress.

We are listening!

We are listening to what you have to say! So feel free to comment, like, share, or whatever! (The part with “like and share” is an actual order!)

Here’s our reddit post for just that!

Aug 062015
 

reddit_alien

Hey guys! A quick minipost!

It’s very important for us to stay connected with you during development. We’ve decided to use Reddit for this as it’s widely used and easy to use.

You can find our subreddit here: https://www.reddit.com/r/vaporum/

Here we can discuss all things Vaporum, whatever interests you, questions, ideas, etc. So stop by, subscribe, and say hi 😉

Next week, we’re going to have a bigger post with some actual meaningful stuff. :)