Learn how to inject randomization into your game to keep players engaged and eager for the next scene. This is the second post by Christo Nobbs in his series on designing systems, expanding on his contributions to The Unity game designer playbook. Check out the e-book for more on how to prototype, craft, and test gameplay in Unity.
In his earlier blog post, Christo touched on how designers can create systems in their games that result in intriguing and unexpected gameplay. In this post he delves deeper with examples on how to set up randomization.
You can entice players into further exploring your game world’s systems, and devise unique experiences for them, with visual prompts. The previous post, Systems that create ecosystems: Emergent game design, outlined a sandbox space wherein everything is made and constructed from wood with the possibility of unpredictable fire propagation. Let’s build on this example by giving players the ability to cut down trees with an axe.
Assume that the landscape where players stand is flat. They don’t know which direction the tree will fall in when they chop it down. What if some trees were “snags” – meaning they’re dead but still standing? They could fall at any moment. An unpredictable element like this in the game will startle the players and intensify their play environment.
You can add any number of visual cues to show players that falling trees are dangerous in this world and keep them on their toes. The clues will help players spot differences between the more dangerous snag trees and those that are healthy and less threatening. They will have to choose how to weigh the risks: Would it be wiser to scavenge for firewood from trees that have already fallen and avoid being injured by the dead trees waiting to fall?
As a designer, be sure to map out where these systemic chain reactions occur, and design into them, supporting when players set them off, such as trees falling unpredictably, catching fire, and thereby spreading delightful chaos.
The Random scripting class in Unity is a static class that provides you with approaches for generating random data in a game. This class has the same name as the .NET Framework class System.Random and serves a similar purpose, but differs in some key ways – one being that it’s 20% to 40% faster than System.Random.
Below are the static properties and methods available with the Random class:
Static properties
- insideUnitCircle: Returns a random point inside or on a circle with radius 1.0 (read-only)
- insideUnitSphere: Returns a random point inside or on a sphere with radius 1.0 (read-only)
- onUnitSphere: Returns a random point on the surface of a sphere with radius 1.0 (read-only)
- Rotation: Returns a random rotation (read-only)
- rotationUniform: Returns a random rotation with uniform distribution (read-only)
- state: Gets or sets the full internal state of the random number generator
- value: Returns a random float within [0.0..1.0] (range is inclusive) (read-only)
Static methods
- ColorHSV: Generates a random color from HSV and alpha ranges
- InitState: Initializes the random number generator state with a seed
- Range: Returns a random float within [minInclusive..maxInclusive] (range is inclusive)
The previous blog post explored the role of design levers and the use of ScriptableObjects to store those values. You can replace those values with appropriately designed ranges using Unity’s Random.Range, which returns a random float within [minInclusive..maxInclusive] (the range is inclusive). Any given float value between them, including both minInclusive and maxInclusive, will appear about once every 10 million random samples.
With this approach you can draw a value from the set range for your result. You’ll have to test several ranges to find the one that works for your gameplay goals, but again, ensure that you design for the defined range you’ve set.
Randomness makes for better immersion. For example, let’s say that each tree has a fixed health count of 100, and each axe strike takes 25 points off a tree’s health. This task will soon become predictable, and therefore, boring. Even if you give the trees a health range of 76 to 100, any given tree will be four strikes away from falling. But trying a smaller range of, say, 75 to 76 provides a greater variety of gameplay outcomes, as a tree will take between three and four hits to fall.
Another way to make this scenario interesting is to indicate health changes through clear visual cues instead of health bars. Doing this will allow the player to learn, through gameplay, approximately how many axe swings it’ll take for a tree to fall. Visual cues add some limited unpredictability that can be balanced and adjusted for the target gameplay. Using the Random class instead of a fixed value enables you to transform a monotonous task into a fun one.
To expand on this example, you could choose to remove a random value between 15 and 25 health points for each axe swing. Doing this makes it so that players can’t easily predict how many swings it’ll take to cut down a tree. They’ll need to rely more closely on visual clues to gauge when a tree will fall; clues like the size of chunks flying from the tree or cracks forming up the trunk, branches falling, sound effects, and so on.
They won’t know precisely when each tree will fall, but over time, as players chop down more trees, they can make educated guesses, ultimately improving their chances at survival.
Randomness aims to give players unpredictable challenges that push them to calculate risk and manage the result.
Let’s look at a few more examples of how to use the Random class.
Imagine a card game with an AI enemy that plays its card solely based on the player’s move. This event would quickly become predictable without randomness as it would return the same result each time.
Even setting probability at 50% is too simple a randomization and would soon become obvious to the player. Instead, try adding layers of randomness based on player actions. Doing this will create intricate systems that offer something more dynamic than choosing between two cards in a pool, or whether or not that card is chosen over one of many from the existing pool.
You can add levels of difficulty to the card table by making the enemy favor certain cards over others; decisions predicated on a value it’s been given, or how complete its composition is before attacking, for instance. A boss’s card can multiply its damage ability when played with other power cards, so the enemy waits until a certain variety of power cards are in hand to ramp up the difficulty for the player. You can add “weight” to such a composition by increasing or decreasing the probability of the boss card being played with certain other power cards.
You can use randomness in different forms for your games. Perlin noise, for example, has natural qualities and generates gradient noise from a seed. Try using it in Cinemachine to create a more organic camera feel for third-person follow cameras.
To try Perlin noise, check out the Starter Assets – Third-person Character Controller, or Gaia packs on the Asset Store, along with documentation on how to use Mathf.PerlinNoise.
In an interview, Chris Butcher, one of the lead engineers on Halo 2 by Bungie Studios, discussed the game’s AI, saying, “The goal is not to create something that is unpredictable. What you want is an artificial intelligence that is consistent so that the player can give it certain inputs. The player can do things and expect the AI will react in a certain way.”
With that in mind, how should you set up AI agents to keep your game feeling unpredictable and alive?
One way to experiment with this is to combine Starter Assets with AI tools from the Asset Store, such as A* Pathfinding Project Pro, a tool that lets you move an AI agent to a given point.
Once the AI agent moves toward the player, the player expects to be attacked. But what if it starts a conversation instead? What about adding more NPCs that move around and blend into the space for a more lively feel? These NPCs could choose points given to them linearly, one by one, or better yet, pick logical points based on a set of rules using the Random class.
Let’s say you have an AI agent that shoots at the player with a weak bow and arrow. Unfortunately for the AI agent, it has to be a certain distance from the character because the maximum range for firing is 10 meters. The AI gets into position in front of the player, 10 meters away, and shoots. It’s not an exciting or ideal setup, especially if you add a second shooter fighting for the same position on the NavMesh.
For a more interesting scenario, choose an area where the enemy should approach the player. Do this by using Random.insideUnitCircle, passing the vector2 result into a vector3 for X and Z axes, and then harness RandomRange for both to get an area with a minimum and maximum radius around the player.
The AI agent should choose a point within an acceptable range around the player and shoot, rather than closely approach the player. To add more excitement to the game with minimal coding, apply this to all AI agents so that they attack the player from multiple angles. The AI action is predictable up to a point because you know that the agents need to be a certain distance away to attack, but you can’t predict the angles from which they will strike.
You can build on this example by giving your AI agents the ability to attack in a melee. Use the same random point from the player with a tighter radius. This way, the AI agents choose from a pool of attacks when it’s time to strike.
The player knows that they can be attacked by a melee formation, but from what direction? Would there be an overarm top-down strike? Or a wide swing from left to right? They would have to read the telegraphing on the animation to determine what’s coming their way.
This scenario can get complex if you have multiple enemy NPCs meant to attack the player at the same time. But if you look at Far Cry 2 by Ubisoft, for example, the player is not attacked by all the enemies at the same time. Different enemies will attack at different times, in random ways. You can learn more about this example and other AI scenarios in this video from Game Maker’s Toolkit.
If you attempted to realistically replicate the results of actions taken from the real world into your game, it would require complex, multifaceted equations or a well-trained ML agent. In the end, it might all look out of place, take a lot of time and technical overhead to implement, and still be difficult to balance and control with understanding, if poorly architected.
Yet by using Unity’s Random class, game designers can create believable results for their scenes with tight control over excitement levels, in less time than it would otherwise take. Randomness, of course, is not always appropriate. Predictability remains essential. But if placed in the right areas, at the most opportune moments, you can deliver unique experiences to your players that make them want to play again and again.