Jump to content
  • Advertisement
  • 06/22/17 07:57 AM
    Sign in to follow this  

    Shift Quantum Systems

    General and Gameplay Programming


    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?EUR? 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?EUR? One thing to note is that the model does not perform any logic when you update its data. It?EUR(TM)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 some links. 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: UCLASS() class SHIFTQUANTUM_API USQCommand : public UObject { GENERATED_BODY() public: USQCommand(); FORCEINLINE const FSQCommandContext & GetContext() const; virtual void Finalize(); virtual bool Do(); virtual void UnDo(); void Initialize( const FSQCoords & coords ); protected: UPROPERTY( VisibleAnywhere ) FSQCommandContext Context; }; I think you will agree it can hardly be less than that. You may notice we don't have a ReDo function, because Do will be used in both scenarios. To execute those commands (and of course to UnDo and Redo them), we have a command manager component attached to our level editor actor. As you can guess, its definition is very simple: class SHIFTQUANTUM_API USQCommandManagerComponent : public UActorComponent { GENERATED_BODY() public: USQCommandManagerComponent(); bool ExecuteCommand( USQCommand * command ); bool UnDoLastCommand( FSQCommandContext & command_context ); bool ReDoLastCommand( FSQCommandContext & command_context ); void ClearCommands(); private: UPROPERTY( VisibleAnywhere, BlueprintReadonly ) bool bCanUndo; UPROPERTY( VisibleAnywhere, BlueprintReadonly ) bool bCanRedo; UPROPERTY( VisibleAnywhere ) TArray< USQCommand * > DoneCommands; UPROPERTY( VisibleAnywhere ) TArray< USQCommand * > UnDoneCommands; }; And its implementation is a classic too. For example, the ExecuteCommand: bool USQCommandManagerComponent::ExecuteCommand( USQCommand * command ) { if ( command == nullptr ) { UE_LOG( LogSQ_Command, Error, TEXT( "Can not execute the command because it is null." ) ); return false; } if ( command->Do() ) { DoneCommands.Add( command ); for ( auto undone_command : UnDoneCommands ) { undone_command->Finalize(); } UnDoneCommands.Empty(); bCanUndo = true; bCanRedo = false; return true; } return false; } 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?EUR(TM)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?EUR?), and when we UnDo the command, we do the opposite (hide it, disable the collisions, and so on?EUR?). Finalize then becomes the function of choice to destroy the actor spawned by the command generator. void USQCommandRegisterBlock::Finalize() { if ( ensure( Block != nullptr ) ) { GetOuter()->GetWorld()->DestroyActor( Block ); } } bool USQCommandRegisterBlock::Do() { Super::Do(); return ensure( GetTileManagerComponent()->RegisterBlock( *Block, Context.Coords ) ); } void USQCommandRegisterBlock::UnDo() { Super::UnDo(); ensure( GetTileManagerComponent()->UnregisterBlock( Context.Coords ) ); } void USQCommandRegisterBlock::Initialize( const FSQCoords & coords, ASQBlock & block ) { Super::Initialize( coords ); Block = █ } There are two things worth noting:
    1. 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.
    2. 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). Here is an excerpt of the definition of this class: UCLASS( BlueprintType, Abstract ) class SHIFTQUANTUM_API USQCommandDataAsset : public UDataAsset { GENERATED_BODY() public: USQCommandDataAsset(); FORCEINLINE bool CanBeExecuted() const; virtual USQCommand * CreateCommand( ASQLevelEditor & level_editor ) const PURE_VIRTUAL( USQCommandDataAsset::CreateCommand, return nullptr; ); protected: UPROPERTY( BlueprintReadonly ) uint8 bCanBeExecuted : 1; UPROPERTY( EditAnywhere ) TSubclassOf< ASQBasicBlock > BasicBlockClass; UPROPERTY( EditAnywhere, BlueprintReadonly ) UTexture2D * UITexture; UPROPERTY( EditAnywhere, BlueprintReadonly ) FText DisplayText; // … }; For example, here is the editor view of a command data asset used to add a game block in the level: image2.png Our command generator assets are grouped by categories, which is another UDataAsset derived class: image3.png 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: image1.png 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: bool ASQLevelEditor::ExecuteCommand( USQCommandDataAsset * command_data_asset ) { if ( command_data_asset == nullptr ) { return false; } if ( !command_data_asset->CanBeExecuted() ) { UE_LOG( LogSQ, Warning, TEXT( "The command %s can not be executed." ), *command_data_asset->GetClass()->GetFName().ToString() ); return false; } LastCommandDataAsset = command_data_asset; auto * command = command_data_asset->CreateCommand( *this ); return CommandManagerComponent->ExecuteCommand( command ); } To finish this part, here is the implementation of the command data asset used to register a game block in the level: USQCommand * USQCommandDataAssetAddGameBlock::CreateCommand( ASQLevelEditor & level_editor ) const { if ( !ensure( GameBlockClass != nullptr ) ) { return nullptr; } // Get needed informations, like the cursor position, the current displayed color, and so on… // Early return if we want to register a game block on coordinates which already has the same game block if ( tile_infos.Block->IsA( GameBlockClass ) && tile_infos.BlockPivotCoords == coords ) { return nullptr; } // Spawn the game block auto * game_block = level_editor.GetWorld()->SpawnActor< ASQGameBlock >( GameBlockClass ); // … and initialize it // Fill the array with all the coordinates the new game block will cover (some blocks are larger than a single tile) TArray< FSQCoords > used_coords_array; game_block->GetUsedTileCoords( used_coords_array ); const auto opposite_color = USQHelperLibrary::GetOppositeColor( world_color ); auto * command = NewObject< USQCommandComposite >( game_mode ); command->Initialize( "AddGameBlock", coords ); // For each coordinate used by the new game block, unregister the existing block FillCompositeCommandWithUnsetCoords( *command, *tile_manager, used_coords_array, opposite_color ); auto * set_game_block_command = NewObject< USQCommandRegisterBlock >( game_mode ); set_game_block_command->Initialize( coords, *game_block ); // Finally, register the game block command->AddCommand( *set_game_block_command ); game_block->FillCreationCommand( *command ); return 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?EUR(TM)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?EUR(TM)s all for today?EUR(TM)s article. We hope you found it useful, and helped you understand a few of the systems we currently use inside our game.

      Report Article
    Sign in to follow this  

    User Feedback

    There are no comments to display.

    Create an account or sign in to comment

    You need to be a member in order to leave a comment

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

  • Advertisement
  • Game Developer Survey


    We are looking for qualified game developers to participate in a 10-minute online survey. Qualified participants will be offered a $15 incentive for your time and insights. Click here to start!

    Take me to the survey!

  • Advertisement
  • Latest Featured Articles

  • Featured Blogs

  • Advertisement
  • Popular Now

  • Similar Content

    • By MaizeDaniele
      I'm following this article suggestions in order to improve my basic gamedev knowledge. 
      I've completed the first game in the list: Pong, using C++ and SFML. The thread suggests to ask for code review after finishing a project, so this is the game repo: 
      Let me know what I should have done differently.
      Thanks in advance,
    • By calioranged
      My question is regarding element array buffer (IBO) binding. 
      I have heard it said that (providing a VAO is already bound) an IBO binding will be saved to the current state of the VAO. Does this mean that instead of binding the IBO henceforth, we can just bind the VAO instead? i.e. as long as the VAO is bound before calling glDrawElements(), the draw call will always work?
      I have provided an annotated code example below, for clarity:
      // ... // VBO: glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glNamedBufferData(VBO, sizeof(data), &data, draw_method) // VAO: glGenVertexArrays(1, VAO); glBindVertexArray(VAO); // Enable and input attributes etc... // IBO: glGenBuffers(1, IBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO); glNamedBufferData(IBO, sizeof(data), &data, draw_method) /* index buffer binding saved to VAO state? as long as VAO is bound prior to draw call, glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO) does not need to be called again? */ // ... while (!display.WindowShouldClose()) { display.ClearBuffers(); // ... glBindVertexArray(VAO); // index buffer binding not necessary? Only VAO binding required? glDrawElements(GL_TRIANGLES, elements, GL_UNSIGNED_SHORT, nullptr); // ... display.SwapBuffers(); }  
    • By phil67rpg
      well  this is very new to me. I want to  use opengl to create a gui window for my game. I am making a rpg indie game.
    • By doug25
      I want to start a team with the goal of creating a simple game
      this will be good project experience for working together
      The game can be 2d or 3d, might have multiple programmers
      I'm thinking a medieval/Egyptian theme fantasy possibly
      I will want one or two writers if needed, and we all may contribute to the storyline
      I think characters and dialogue can work really well-Zelda,Diablo,Neverwinter nights for example
      Looking for
      Sound ok?
      My name's dougie
      If you're interested contact me here on the forum or email me:
      sound_of_solos@hotmail.com, but beware I might not see your email because my inbox is loaded.
      I'm a C++ programmer and I use Unreal Engine as my engine of choice
      so I'll be the project lead most likely
    • By jnbutler
      How do you make it where the program itself does not exit until both windows are closed. I have a project and created two windows using two different window classes two different window handles and two different winprocs, two boolean variables hwnd0Closed and hwnd1Closed. Then inside each of the winprocs in the WM_CLOSE handlers, I have an if statement that tests the two boolean variables
      hwnd0Closed = true; if(hwnd0Closed && hwnd1Closed) { PostQuitMessage(0); } DestroyWindow(hwnd);  This is the one for hwnd0 in WindowProc0. A similar one is in WindowProc1 but sets the hwnd1Closed  = true. 
      This should not close the whole program until both the windows have been closed, but when I click on one of the windows X to close it, both of them close and the program shuts down. I have attached my full source code file.
      Thanks for any help you can provide.

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!