In this article, we will cover a few aspects of the systems implemented in the code base of Shift Quantum. From how blocks composing the levels are managed, to our command system for the in-game level editor, including how we generate those commands from the HUD, you will have a better idea of how we handle all of this in code.

Separation between model and the view

Because basically our levels are a grid of blocks, early on in the development of the game we thought it would be best to separate the data from its representation. This would have several advantages:

The game would only have to manipulate some raw and abstract data without having to take care of where to place the blocks for example.

It's possible to have multiple representations of the model: a 3D representation like now, but also a complete 2D sprite based view if needed ( to generate a map for big levels for example ).

It is very easy to write unit tests to validate that the data is correctly handled by the model, enforcing the validity of the various operations on the model throughout the development.

The model contains all the information related to the level, like the width and height, the current color space (Black or White), the rotation of the grid, or all the blocks coordinates. It also contains information used only by the in-game level editor, like the cursor coordinates and rotation.The model communicates with the other classes using events. Lots of events... Here is a non exhaustive list of the names of the events to give you a rough idea: FOnInitializedEvent, FOnWorldRotationStartedEvent, FOnWorldRotationEndedEvent, FOnWorldColorUpdatedEvent, FOnBlockRegisteredEvent,FOnBlockUnRegisteredEvent, FOnDimensionsUpdatedEvent, etc…

The other classes which want to know when something happens in the model just need to subscribe to those events and react accordingly. For example, the level view is interested for example in knowing when the dimensions of the grid are updated, to adjust the boundaries around the play space, or when a block is registered, so the view can move the block to its correct location in the 3D world (because the model does not even know the blocks are in a 3d space), and so on…

One thing to note is that the model does not perform any logic when you update its data. It’s up to the caller to take care of removing an existing block before adding a new one at the same coordinates. More about that later in the commands section.

Level Editor Commands

During the incubation meetings, maybe the first answer given to the question "What do you think is important to have in an in-game editor?" was an Undo-Redo system for all the operations available.So we obviously had to implement a command pattern. This is a very well-know pattern, so I don't think we need to dive a lot in the details. But if you need a refresher, here are somelinks.Nonetheless, I can give you one advice: your command classes must be as lightweight as possible. No logic should happen in the commands, because the more logic you put inside, the more complicated it will be for you to handle the Undo and Redo parts of the contract.As such, the declaration of the base command class is very simple and straightforward:

One function which deserves a bit of explanation is Finalize. You can see this function is called on commands which have been UnDone at the moment we execute a new command, before we empty the UnDoneCommands array. This allows up to do some cleanup because we know for sure the commands won’t be executed again.

For example, when we want to add a new block in the world, the command generator will spawn that block, initialize it, and pass the block as a pointer to the RegisterBlock command. When we Do that RegisterBlock command, we just kind of toggle on the block (make it visible, set up the collisions, etc…), and when we UnDo the command, we do the opposite (hide it, disable the collisions, and so on…). Finalize then becomes the function of choice to destroy the actor spawned by the command generator.

Because the command manager only executes one command at a time, and because any action we do is made of several commands (for example, adding a block in the grid is made of at least 2 commands : remove the existing block at the cursor coordinates, then register the new block), we have a special command class named USQCommandComposite. It stores an array of sub-commands. Those sub-commands are executed linearly in Do and in reverse order in Undo.

You may have noticed the FSQCommandContext structure. Each command holds a context internally, which is used to store some global informations at the moment the command is initialized (just before being executed for the very first time). This allows us to restore the cursor position and the zoom level of the camera when we undo / redo any command, allowing the player to have the editor in the same state as it was when he first executed an action.

Command generation

Because our commands are very simple, we need to put the logic elsewhere. And because we need a way to bind the generation of those commands to the UI, we created class hierarchy deriving from UDataAsset. This allows us to make properties editable in the UE4 editor such as the sprite to display in the HUD, the text to display under the sprite, the static mesh to assign to the cursor, or the maximum number of instances of a given actor which can be spawned in the level (for example, we only allow one start point and one exit door).

For example, here is the editor view of a command data asset used to add a game block in the level:

Our command generator assets are grouped by categories, which is another UDataAsset derived class:

In the in-game editor UI widget, we just iterate over all the commands of the selected category, and add new buttons in the bottom bar:

When the player presses the A button of the gamepad, we know the selected command data asset. Now, we just need to create the command out of this data asset, and give it to the command manager command:

As you can see, there is a lot more logic than inside the various commands, because here, we take care of all the steps needed to execute a final command. As mentioned in the first part, the model does not perform any logic related to the integrity of its data.

It’s then up to the command generator to make sure for example that all the coordinates of the grid which will be covered by a new game block are first cleared up.

This can expand quickly, because maybe one of the coordinates to clear is part of another game block. Then the complete game block must be removed too. But as we can not leave holes in the grid, we must in a last step fill the holes left by removing this game block, but not covered by the game block we want to add, by basic blocks.

This gets really hefty in the command generator which allows to resize the level, as you can imagine. And this is where being able to unit-test the model comes in very handy. (VERY!)

That’s all for today’s article. We hope you found it useful, and helped you understand a few of the systems we currently use inside our game.

Post a comment

Sign in or join with:

Only registered members can share their thoughts. So come on! Join the community today (totally free - or sign in with your social account on the right) and join in the conversation.