Jump to content
  • Advertisement
Mapet

Game code architecture

Recommended Posts

Hi, I started implementing 2D board game. I have concept of how to write rules, controlls etc, but i dont want to write another all-in-app. So i decided to do it "right". I divided my code into reuseable modules - ResourceManager, Renderer, Core, Math (for now). All modules use SDL2.

ResourceManager(RM) - loads textures, audio etc without duplicating them in memory. Resources are gathered in TextureResource, AudioResource (...) objects, handled by std::shared_ptr. For textures I have prepared Texture class that wraps SDL_Texture and I RM serves this Texture objs for Core module.

Core - The main game module, contains game loop, gameobject / component implementation and event handling. Core requests for data from RM and sends them to right components.

Renderer - Creates window and knows about render range (in core represented by camera). Takes info about texture, position, rotation and scale to render images (just this for now).

Its time for my questions:

  • is this architecture good? After I end this board game I want to extend renderer module for example for rendering 3D objects. 
  • Loading resources while ingame is good idea? I mean single textures, models, sounds etc. 
  • As I said, for handling resources I am using shared_ptr, is it good cleaning cache every (for example) 3 minutes? By cleaning i mean removing not used resources (counter =1).
  • And the hardest thing for me right now - take a look at this flow:
    • Core create a T1 token
    • Component Renderer2D is connected to T1.
    • Core requests a texture /textures/T1.png from RM.
    • RM checks if /textures/T1.png is in map, if not, loads it.
    • RM returns a std::shared_ptr<Texture> to Core.
    • Core assign texture to T1 Renderer2D component.
      Now i want to pass this object to renderer. But I wont pass all gameObjects and checks which have renderer2D component (i also cant, because only Core know what is gameObject and component). So i had an idea: I can create Renderable interface (in Renderer module) and inherit from it in the renderer2D component. Renderable will contain only pointers to position data. Now i am able to pass renderer2D component pointer to Renderer and register it.

      Is this good way to handle this? Or im overcomplicating things?
  • If point above is right I had last question - registering object in Renderer module. I dont want to iterate over all objects and check if I can render them (if they are in render range). I wanted to place them in "buckets" of - for example - screen size. Now calculating collisions should be faster - i would do this only for objects in adjacent buckets. But for 2D game i have to render objects in correct order using Z index. Objects have to be placed in correct bucket first, then sorted by Z in range of bucket. But now i have problem with unregistering objects from Renderer module.
    I think I got lost somewhere in this place... Maybe You can help me? Of course it this is correct way to handle this problem.

I would love to read your comments and tips about what can I do better or how can i solve my problems.
If i didnt mention something but You see something in my approach, write boldly, I will gladly read all Your tips :).

Share this post


Link to post
Share on other sites
Advertisement

In general your approach of project architecture is as good as any other approach as long as it works for you and dosen't cause circular dependencies. This would be the case if Renderer needs to now stuff from Core and vice versa so what I do when creating such a project (and my current game engine architecture looks the same) is to think about usages. All modules stay on the same layer of dependencie and If you know code isn't used by any other module on the same layer that's fine, otherwise you should move it one layer back to have anything depending on it in a layer above it.

This sounds very complicated but as I rewrote and 'm still rewriting my build tool (I run my own build tool in C# to create and build my C#/C++ projects), I also added some kind of code analysis to have an overview over my code. This produces diagrams like this one

Screenshot.png.7dac397fa59c353a6209eedd6fc8dea5.png

So as you can see, the dependency graph shows relationship between classes (this is as C# project so don't worry about circular dependencies between classes) and could also point to external classes from other projects in the same solution.

I did this for the exact same reason, to know what modules and especially what code depends on other code to always have an overview of my architecture.

17 hours ago, Mapet said:

Loading resources while ingame is good idea? I mean single textures, models, sounds etc.

Yes you can do this. Take a look for example at the Pokemon TCG App (it is free to play so feel free to do so). When you play a card that isn't cached yet, the App downloads the resource from their server and a progress bar appears on the cards. I think your delay wouldn't be that straight but it is absolutely valid.

Most games put their assets into a package so you minimize disk access time (that is the most performance impact when loading assets) and for example use Memory Mapped Files for fast parallel reading access. This is just a tip if you feel your asset loading is too slow ;)

17 hours ago, Mapet said:

As I said, for handling resources I am using shared_ptr, is it good cleaning cache every (for example) 3 minutes?

You shouldn't use shared_ptr here if you want cache cleanup. The struct implies that it will handle Garbage Collection for you but that isn't true. When loading a texture, you usually load it directly into your graphics card rather than keeping a pointer on your heap and in this case shared_ptr won't help anything because it is a heap structure not a VRAM one.

What I do in this case is rolling my own handle that can also take some more actions into account than just simply hold a ressource reference. My RessourceHandle struct for example that keeps a reference to some default texture. This handle is returned immediately from the asset manager while it also starts loading the asset in background. The renderer could just use the asset and behave as normal while the real asset is loaded in background and flipped once the process is ready.

17 hours ago, Mapet said:

And the hardest thing for me right now - take a look at this flow:

  • Core create a T1 token
  • Component Renderer2D is connected to T1

 

If you already use some kind of Components, why not driving the ECS path and just let your renderer work on lists of components instead of passing knowledge about GameObject to it? You could maintain component relations in different concurent arrays of components of the same kind and let your renderer just pick those arrays to do a linear iteration and render pass over them. This would not just save management overhead but also time because you won't need to pick every game object, fetch its contents from memory to just fetch it's mesh, texture and whatever from memory again causing a possible CPU cache replace

Share this post


Link to post
Share on other sites

Hello, first of all, thx for anwsers, it was really informative!

@Shaarigan

About shared pointers -maybe I wasn't clear enough- I am using them for count how many gameobjects using specific resource. I'm creating pointer for Texture object which contains SDL_Texture allocated once. I made wrapper because I wanted to have easy access for some information not included in SDL_Texture struct (like image size or if alpha is used). Now, if reference count drop to 1 for shared_ptr I know I can clean GPU memory using SDL specific function. Thats why I decided for using shared_ptr.

As You said, I'm trying to avoid circular dependencies. Thats why I wont send component to Renderer (because components are stored in Core module). But I think I can create interface in Renderer module and implement it in renderer2D component. Now I can apply Your advice to my code. I dont know yet if I will send whole list of objects to Renderer or register them one at time, but I am sure I will find solution.

@Makusik Fedakusik

I heard about it earlier, but now I have to read something more about this.

Share this post


Link to post
Share on other sites

You misunderstand ECS. In ECS, your Components are known by the system but not vice versa so if you have a centralized storage that keeps your components and entity relations in Core, your renderer can access any component it needs but your Core won't need to know renderer. Why should it?

Share this post


Link to post
Share on other sites

Hello again after long break. Job stopped me from diving into my project, but I have come back :).

I read articles about ECS and it looks promising, but i have several questions about it. I think that i catch the idea of ECS, but few implementation aspects are unclean to me.

In first place in a few sentences I will say how I understand ECS:
1) Each game object is an ECS entity. The Entity have just ID and a list of components.
2) The Components are just sets of data containing selected pieces of information (for example PositionComponent will have just float x,y,z position, RotationComponent - float rad/deg/quaternion etc.)
3) Systems are logical units that process all Entities which contain selected components (for example RenderingSystem will require TransformComponent and TextureComponent).

So there are not really another classes for each game object like enemy or spawn point, but I have to just add required components to new Entity.

I can "save" entity as prefab, so I can quickly reproduce game objects without adding new components manually.

 

Now my questions. I have some ideas, of course, but it would be great to read your recommendations.
1) If im correct, I need something like EntityManager class that contains all entities, allows to create new ones, destroying unwanted and so on. 
2) I think, i cannot check in each system every entity in terms of containing specific components. How should I do it correctly? Maybe should I create lists for each type of components and keep references / entity id / component id? It is even possible to automatize? I mean i could write it like that, but then if I add another component I have to remember to create list corresponding to it. This can be in my opinion optimized, but I dont know how at this moment.
3) Where logic of selecting entities for computations in specific system should be? In each system seperately? 
Somehow select entity that have required components -> processing -> do it while there still are entities
or
Create a SystemManager where systems are registered and loop over all entities and process them in required systems?
This looks nice, but I have feeling that I should process each system one by one, not all systems for each entity (but thats just feeling).
4) Specific systems and components should be part of my ECS Module? I mean lets suppose im writting two different games using my ECS Module. First game is 3D game, second 2D. In first game TransformComponent will have float x, y, z fields, in second - float x, y.
I know that "perfect is the enemy of good" but I'm wondering, how could I go around this?
5) How systems are communicating? Is it even required? For example i have InputComponent in which I have list of pressed keyboard keys. How should I change its values? In InputSystem which reading keyboard and updating list od pressedKeys? But then I need good order of executing systems mentioned before (input have to be processed before anything other or I will get 1 frame delay which can be annoying in for example hardcore platforming game).

Thats all my ideas, questions and thoughts. Tommorow i will start writting something, but I am looking forward to your advice.

Share this post


Link to post
Share on other sites
On 12/13/2018 at 3:20 AM, Mapet said:

1) If im correct, I need something like EntityManager class that contains all entities, allows to create new ones, destroying unwanted and so on

You could do so but it isn't necessary at all. I myself stored that part of logic in the Entity itself. The idea of ECS is great in theory but in practice there could be situations when you have to implement it a bit different for reasons

On 12/13/2018 at 3:20 AM, Mapet said:

2) I think, i cannot check in each system every entity in terms of containing specific components. How should I do it correctly?

What I did here is to have some templated class that is also stored in a global static variable. This template class contains some logic that helps to find free space in a continous array of exactly one type of component and will potentially increase the space if needed. Each entity has an ID that is nothing less than the zero-based index in that array.

My systems now grab the array pointer of the desired components and traverses based on the entity IDs through the array so your CPU might work a lot faster with that component pairs.

For the Entities, you could do something similar to what Unity currently does and maintain some sort of cache in your systems. The so called ComponentGroup contains IDs of all entities that match certain criteria.

I'm sad that I can't show you concrete code examples at the moment so I need to do some pseudo code

namespace Entities
{
   template<typename T> struct ComponentCache
   {
      private:
         T* dataPointer;
         uint64 freeSpaceIndex;
     
      public:
         T* AddComponent()
         {
            uint64 index = freeSpacePointer;
            freeSpacePointer++;
            return dataPointer + index;
         }
   }
   template<typename T> ComponentCache<T> Components()
   {
      static ComponentCache<T> components;
      return components;
   }
}

void Main
{
   Transform* t = Entities::Components<Transform>().AddComponent();
   t->Position(Vector3::Zero());
}

void MoveAll(Array<uint64>& entities)
{
   const uint64 size = entities.Size();
   for(uint64 i = 0; i < size; i++)
   	(*(Entities::Components<Transform>().Components + entities[i])) += Vector3::UnitZ();
}

This also mostly answers your question 3 and 4 :)

On 12/13/2018 at 3:20 AM, Mapet said:

5) How systems are communicating?

Systems communicate usually through events if neccessary. Think of your InputSystem wants to tell the PlayerControllerSystem that there was some input required and actions need to be performed. InputSystem will get the pressed button and evaluates the action against all entities that have the input receiver component attached to. It checks the component and finds the "MovePlayer" action on exactly that button pattern it received right now. Then it will fire the MovePlayer state to the GameAction-EventChannel. The PlayerControllerSystem listens to that channel and enqueues an update into your game loop

Edited by Shaarigan

Share this post


Link to post
Share on other sites

Thank you for all your answers. 

if I encounter any problems (and it will definitely happen ^^) I will come back with more questions for sure.
At the moment I think you've helped me so much that I can try to design my ECS module correctly enough :).

Share this post


Link to post
Share on other sites

  • Advertisement
×

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!