Jump to content
  • Advertisement
  • Remove ads and support GameDev.net for only $3. Learn more: The New GDNet+: No Ads!

  • 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
  • Advertisement
  • Latest Featured Articles

  • Featured Blogs

  • Popular Now

  • Similar Content

    • By Shaarigan
      I'm currently starting next iteration on my engine project and have some points I'm completely fine with and some other points and/or code parts that need refactoring so this is a refactoring step before starting to add new features. As I want my code to be modular to have features optional installed for certain projects while others have to stay out of sight, I designed a framework that starting from a core component or module, spreads features to several project files that are merged together to a single project solution (in Visual Studio) by our tooling.
      This works great for some parts of the code, naming the Crypto or Input module for example but other parts seem to be at the wrong place and need to be moved. Some features are in the core component that may belong into an own module while I feel uncomfortable splitting those parts and determine what stays in core and what should get it's own module. An example is Math stuff. When using the framework to write a game (engine), I need access to algebra like Vector, Quaternion and Matrix objects but when writing some kind of match-making server, I wouldn't need it so put it into an own module with own directory, build script and package description or just stay in core and take the size and ammount of files as a treat in this case?
      What about naimng? When cleaning the folder structure I want to collect some files together that stay seperated currently. This files are foir example basic type definitions, utility macros and parts of my Reflection/RTTI/Meta system (which is intended to get ipartially t's own module as well because I just need it for editor code currently but supports conditional building to some kind of C# like attributes also).
      I already looked at several projects and they seem to don't care that much about that but growing the code means also grow breaking changes when refactoring in the future. So what are your suggestions/ oppinions to this topic? Do I overcomplicate things and overengeneer modularity or could it even be more modular? Where is the line between usefull and chaotic?
      Thanks in advance!
    • By PlanetExp
      I've been trying to organise a small-medium sized toy game project to supports macOS, iOS and Windows simultaneously in a clean way. But I always get stuck when I cross over to the target platform. I'll try to explain,
      I have organised my project in modules like so:
      1. core c++ engine, platform agnostic, has a public c++ api
      2. c api bindings for the c++ api, also platform agnostic, this is actually part of 1 because its such a small project
      3. target platform bindings, on iOS and macOS this is in swift. Basically wraps the c api
      4. target platform code. This part just calls the api. Also in swift.
      So in all I have 4 modules going simultaneously, all compiled into a separate static libraries and imported into the next phase/layer. Am I even remotely close to something functional? I seem to getting stuck somewhere between 2 and 3 when I cross over to the target platform. In theory I would just need to call the game loop, but I always end up writing some logic up there anyway.
    • By SmilingDerek
      I've decided to branch out from Java and try learning C++ with Vulkan because it sounded like fun, but I've had a lot of trouble linking the libraries for GLFW and Vulkan. I'm following a tutorial on vulkan-tutorial.com which uses Visual Studio for the project. I've tried Visual Studio, Eclipse CDT, Code::Blocks, and command line compilation using MinGW G++, but every single method complains about undefined references to GLFW and Vulkan members. Here's the code in main.cpp:
      #define GLFW_INCLUDE_VULKAN #include <GLFW/glfw3.h> #define GLM_FORCE_RADIANS #define GLM_FORCE_DEPTH_ZERO_TO_ONE #include <glm/vec4.hpp> #include <glm/mat4x4.hpp> #include <iostream> int main() { glfwInit(); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window", nullptr, nullptr); uint32_t extensionCount = 0; vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); std::cout << extensionCount << " extensions supported\n"; glm::mat4 matrix; glm::vec4 vec; auto test = matrix * vec; while(!glfwWindowShouldClose(window)) { glfwPollEvents(); } glfwDestroyWindow(window); glfwTerminate(); return 0; } Here are the commands being used to compile the code:
      g++ -std=c++11 -fexceptions -g -IC:/glfw-3.2.1/include -IC:/glm- -IC:/VulkanSDK/ -c "src/main.cpp" g++ -LC:/glfw-3.2.1/lib-mingw-w64 -LC:/VulkanSDK/ -o VulkanTest.exe main.o -lglfw3 -lvulkan-1 The first command compiles the .cpp into a .o successfully, but the second command gives me errors from the linker. Every single reference I made to a member from Vulkan or GLFW is undefined. (Path has been shortened for easier reading)
      [omitted]/src/main.cpp:12: undefined reference to `glfwInit' [omitted]/src/main.cpp:14: undefined reference to `glfwWindowHint' [omitted]/src/main.cpp:15: undefined reference to `glfwCreateWindow' [omitted]/src/main.cpp:18: undefined reference to `vkEnumerateInstanceExtensionProperties@12' [omitted]/src/main.cpp:26: undefined reference to `glfwWindowShouldClose' [omitted]/src/main.cpp:27: undefined reference to `glfwPollEvents' [omitted]/src/main.cpp:30: undefined reference to `glfwDestroyWindow' [omitted]/src/main.cpp:32: undefined reference to `glfwTerminate' It seems like the linker can't find the library files I provided with -L and -l, but if I change -lglfw3 to -llibglfw3.a or -lglwf3.dll, I get this:
      [omitted]/mingw32/bin/ld.exe: cannot find -llibglfw3.a
      [omitted]/mingw32/bin/ld.exe: cannot find -lglfw3.dll Leading me to think that the linker DID find the libraries at first, since it didn't complain about being unable to find the library - but why can't it find sources for the references to GLFW / Vulkan functions? I have no idea what's happening. Is it finding the library files?
      I'm using GLFW 3.2.1, Vulkan SDK, MingW GCC version 6.3.0, and I'm running on Windows 10 Pro 64-bit.
    • By mychii
      Hi, now my curiosity is stuck on arrow operator. Is there a way to overload an arrow operator from a pointer object? If none, what's the reason that this isn't allowed? or is there a correct/better approach?
      class B { public: void foo() {} }; class A { public: B* b; B* operator->() { return b; } }; int main() { A a; a->foo(); // Works. A* pa = new A(); (pa->operator->())->foo(); // Works. pa->foo(); // Doesn't work as it refers directly to A's foo() which doesn't exist instead of triggering operator->() first. return 0; } Cheers. 😁
    • By MarkNefedov
      So, initially I was planning to create a base class, and some inherited classes like weapon/armour/etc, and each class will have an enum that specifies its type, and everything was going ok until I hit "usable items".
      I ended up with creating UsableItem class, and tons of inherited classes, like Drink/Apple/SuperApple/MagickPotato/Potion/Landmine/(whatever that player can use) each with unique behaviour. I planned to store items in the SQLite database, but I discovered that there are not many ways of creating variables(pointers) with type determined at runtime (that preferably get their stats/model/icon/etc from DB). So, I think that I need to use some variation of the Factory pattern, but I have no idea how I should implement it for this particular case (giant switch/case 😂 ).
      It would be really nice if you guys can give me some advice on how I should manage this kind of problem or maybe how I should redesign the inventory.
      Inventory storage is an array of pointers. I'm working with CryEngine V, so RTTI can't be used.
      Example code:
      namespace Inventory { enum ItemType { Static, Building, Usable, Weapon, Armour }; class InventoryItem { public: virtual ~InventoryItem() = default; virtual ItemType GetType() = 0; virtual string GetName() = 0; virtual string GetIcon() = 0; virtual void Destroy() { //TODO: Notify inventory storage delete this; } }; class UsableItem : public InventoryItem { public: struct Usage { int Index; string Use_Name; }; virtual CryMT::vector<Usage> GetUsages() = 0; virtual void UseItem(int usage) = 0; }; class TestItem : public UsableItem { int Counter =0; ItemType GetType() override { return ItemType::Usable; } string GetName() override { return "TestItem"; } string GetIcon() override { return "NULL"; } CryMT::vector<Usage> GetUsages() override { CryMT::vector<Usage> Usages; Usages.push_back(Usage{1, "Dec"}); Usages.push_back(Usage{2,"Inc"}); Usages.push_back(Usage{3,"Show"}); return Usages; } void UseItem(int usage) override { CryMT::vector<Usage> uses = GetUsages(); switch (usage) { case 0: for (int i =0; i<uses.size(); i++) { CryLog(uses[i].Use_Name); } break; case 1: Counter--; CryLog("Dec"); CryLog("%d", Counter); break; case 2: Counter++; CryLog("Inc"); CryLog("%d", Counter); break; case 3: CryLog("%d", Counter); break; default: CryLog("WRONG INDEX"); break; } } }; }  
  • Advertisement

Important Information

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

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!