Thank you for such a kind review! It's... Cinematic :)
Recent community posts
Create randomized levels with prefabricated rooms is a great way to increase replayability and is often found in what is known as the Rogue-like/lite genre. Given a collection of rooms, we need to find a way to connect them in a logical manner while maintaining a target room count. In Rouge Star Rescue we’ve devised an algorithm that ensures the player must complete a minimum amount of rooms before clearing the level.
The Main Chain
The main room chain is the shortest path the player can follow, from the start room to the end room. At a high level the algorithm for this is as follows:
— Start by placing a random start room at 0,0.
— Find an unconnected door in that room.
— Create a hallway with limited length from that door, check that it doesn’t overlap with any previously placed rooms or hallways.
— Attach a new random room to the end of that hallway (check that it doesn’t overlap).
— Repeat this loop, treating the newly placed room as the start room, for X amount of times. X is the length of the chain.
(The main chain, of length 8, start room at the top, end room at the bottom)
Although the main chain is fully randomized, it still doesn’t create very interesting gameplay. We don’t want the player to just go from the start room to end room. We want the player to be able to explore around. In roguelite games it shouldn’t be immediately obvious which door the player should choose. There has to be some side and backtracking to maintain a sense of mystery.
To fix this we add branch rooms to the main chain. These are rooms that extend from the main chain rooms. The high-level algorithm is as follows:
— Choose a room in the existing chain.
— Check if it has any unconnected doors.
— If yes, create a hallway, check that it doesn’t overlap.
— If the hallway is valid, place a random room at the end of it, check that it doesn’t overlap.
— Repeat this for each room in the main chain.
Additionally, you can choose to branch even more by repeating the same algorithm on the branch rooms. You can continue to loop over them as many times as you want, creating a large branch out effect. In our game we specify MAX_ROOMS for each level, and the branches will continue to loop over itself until the MAX_ROOMS number is achieved.
(The main chain with 4 branches added)
Branching has gone a long way to create more interesting levels. However after the player plays the game for a while, he might notice that there is a pattern that feels like he’s in a tree with branches. We want to eliminate any chance of the player being able to predict which room to go to for the shortest level completion. For this we add post linking hallways. The high-level algorithm is as follows:
— Loop through each generated room
— check if that room has an unconnected door
— if yes, check if there is another room with an unconnected door nearby
— if yes, make a hallway between them (check for overlaps).
It’s a fairly simple algorithm but the effects of this on gameplay are profound. With this the level generates loops that can lead a player in an exploration circle. Optionally you can check that you are only post linking two rooms that are at a similar degree in the chain generation. For example, you don’t want to post link the second room in the chain with the 7th room in the chain, since by doing so the player will be able to bypass a large part of the chain. In Rogue Star Rescue we ensure that only rooms with a chain degree of +/- 2 can post link.
(Post linking hallways has created a nice loop around chain link #2 and #3)
The details of this level generation implementation are complicated and we will cover more detailed aspects of it in the future. Use this high-level plan to create interesting levels of your own.
Fast-action twin-stick shooters can be made much more enjoyable with aim assist. Aim assist is used to detect a target in a narrow search area, and then adjust the aim of the gun to point directly at the found target. Admittedly this feature isn't for everybody, but for most controller players it's a welcome addition. In the past couple weeks we've examined a few different approaches to implementing this in Unity.
Searching for a target
Circle Cast 2D Approach
The Unity documentation states: A CircleCast is conceptually like dragging a circle through the Scene in a particular direction. Any collider making contact with the circle can be detected and reported.
At first glance this might seem like an ideal solution for finding targets. Though we've found this has some shortcomings.
First, even though the circle cast has a variable circle radius it will still only detect one collision (unless you use CircleCastAll() but then you have to manage distance sorting logic). Imagine an enemy right next to a wall. If the circle cast is wide enough, it might hit the wall before hitting the enemy and hence not provide the expected target.
Secondly, the circle cast has a constant radius throughout its cast. Which means you cannot make cone-shaped casts. Cone shapes are very useful for aim assist since they effectively make the search area wider at further distances and narrower at closer ranges. Conceptually this is better as it leads to a less jerky experience at close range, and better target finding at a distance where it is most needed.
Angled Multi-Raycast Approach (Cone)
A better solution is to use many raycasts set off from the player. This way we can evaluate each ray individually and set them off in a cone shape. The code that demonstrates how this is achieved can be found here.
This function takes the visibility thickness as a parameter. It's useful to tie this to a persistent setting. In Rogue Star Rescue we're including a slider in settings which adjusts this from 0 to 100%, which is effectively the strength of the aim assist. This has the added bonus of letting users disable it completely, many mouse/keyboard players prefer that.
This function is called every frame in FixedUpdate from our main Player Controller. You can see the number of raycasts can be adjusted, although we found 5 to be the optimal amount. More raycasts will have a better search precision, but will cost more in terms of performance. Since this is called every frame, it's better to keep it fast.
We also want to make sure aim assist isn't active when an enemy is too close to the player (this creates a jerky effect). We use the minDistanceAway to ignore any targets that are too close. Also when evaluating distance we use the square of the distance between objects. Without getting too technical, it's much more efficient to do it this way since it avoids the costly square root calculations of Pythagorean's theorem.
Using the multi-raycast approach produces smooth and effective aim assist. This is a big part of the 'feel' of a game so it's important to fine tune the parameters while still leaving an adjustable strength in the game settings.
Found a target
This is what I learnt while developing Rogue Star Rescue, a 2D rogue-lite sci-fi shooter with defense elements.
For many roguelike (or rogue-lite) games, developers choose to handcraft their rooms. This creates a super polished feel and a generally more enjoyable play experience. In non-procedural games this takes less time since the number of rooms or scenes is typically less than a hundred.
But what about designing games with hundreds or thousands of unique rooms? Imagine carefully designing these large sets of rooms over the course of months, to find out that a key feature of each room needs to be changed (such as a collider or door structure). Or that we need to replace all the rooms with a completely different tileset. In Rogue Star Rescue we’ve developed a system that allows us to invest weeks of time in good room design, but without tying it to one set of rules.
Instead of sitting down and designing each room directly. Let’s create an abstraction layer between the shape of the room and its stylized features. For this we create a MarkerSet. A MarkerSet is a simple tile set that defines the key features of a room, with no direct styling. Here’s an example of a simple marker tile set, and how it is used to design a room:
The logic for the markers is as follows:
Solid Orange Tile = Wall Marker
Light Gray Tile = Floor Marker
Dark Gray Tile = Pit Marker (not used in this example)
Diagonal Orange Tile = Door Marker
S and E Tiles = Special Start and End markers (not used in this example)
It’s good to solidify this logic by making a MarkerSet component class, in which each property holds a reference to the tile’s meaning.
To separate the sprite sheets from the room generation logic, we create a MyTileSet class that stores the information of what each tile *is*. As you can see below we define several fields in the class that dictate whether a tile is a north wall, pit, wall variant, floor etc. This is also very useful when working with artists and having to regularly update sprite sheets. This doesn’t lock our room logic to the positioning of a sprite in a sprite sheet.
To turn this into a finished room we will need unity editor scripts to transform our marker set. This can be done in many ways, but the basic idea is:
(MarkerTileMap + MarkerSet + MyTileSet) -> Editor Generation Logic -> Finished Room
The editor generation logic will use different logic to generate a finished room. A simple example of some logic will be for floors. The generation logic can search through each tile in the MarkerTileMap, check if it is a ‘floor’ tile. If so it can produce a floor tile in the same location on the finished room (with the value specified in MyTileSet).
Wall generation can get more complicated. When you detect a Wall Marker in your MarkerTileMap you will need to perform additional logic to check what type of wall will be placed. This is done by checking what other tiles are around the wall tile in question. For example, a wall tile in your MarkerTileMap may have a floor tile to the south of it, and no tile to the north of it. This would mean a NorthWall should be generated in the finished room. The logic for this type of generation can be very complicated, depending on your game style and view perspective. The main thing to remember here is that we want to keep the MarkerSet and MarkerTileMap as simple as possible so that we can generate rooms quickly. Let the editor generation logic do the hard math of what specific tile should be placed.
The beauty of this system is that if we want to change our room style to something completely different. We simply need to change the MyTileSet used for generation. Here is the above MarkerTileMap generated in two different styles:
This technique will save your sanity if you ever have to make major structural changes to your rooms. Use abstraction wisely and minimize your development risk.