0
\$\begingroup\$

I'm actually developing a very simple 3D game, Arkanoid/Breakout style, with very simple mechanics, and it's progressing while I learn the game dev domain own concepts and techniques (My background is C/C++ middleware/low-level systems programmer).

I've advanced to a functional game loop, input, movement, collision checks, all it's working, but I reached to a day where the code was a mess, e.g: my Level class was doing too much. So I partitioned, OOP style, the code in several classes.

My intention is to get ideas from more experienced programmers regarding how Game state and substates relate with entities. My general picture is:

  • A game object which contains the main loop
  • A level object which contains a World instance with a list of all entities.
  • A renderer class which does Direct2D and Direct3D rendering.

Prior to the major refactoring, I was doing something like

Level::Update(float clockMs)  {
  switch (m_state) 
    case (LevelState::Preloading) { ... }
    case (LevelState::Load) { ...}
    case (LevelState::Playing) { 
        UpdateBallPosition ( ... ) 
        CheckCollisions( ...)
    }

}

Level contained references to ball, game bricks, etc. Now I create the entities in the Level startup and add them to the World instance. (World is a member of Level).

Now I want to move all state-related code to the entities themselves. Suppose "Ball", it shouldn't do anything until Level is in LevelState::Playing. Where do you store typically state for the current Level (suppose a closed-level game)? It's correct to store entities with a pointer to the World and the World to mantain a pointer to the Level containing it? It's a tightly coupled architecture but I want to discuss how to relate world/level states with entities, and how the entities acknowledge in which higher-level states they are.

Keep in mind that I accept good procedural or data-driven approaches. I'm not an OOP fanatic, I think good designs can be built upon several paradigms. Thanks.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

In your case, it doesn't make a bit of difference to Ball which world it's in or which Level that World is in.

The only things that matter to the World/Level/GameState/whatever are:

  1. Are there any breakable bricks left?
    -If not, YOU WIN and/or NEXT LEVEL
    -If so and !paused, Ball.Tick()

The only things that matter to Ball are:

  1. Level-based velocity
  2. Am I off the screen? (GAME OVER)
  3. Am I colliding with a brick?
    -If so, is it breakable?
    -If so, Ball calls Brick->Break()
    -If not, bounce (wall bricks)

The only things that matter to Paddle are:

  1. Should I move left?
  2. Should I move right?

The only things that matter to Brick are:

  1. Am I breakable?
  2. Provides bounding box to Ball upon request
  3. "Breaks" upon request from Ball

I've probably left something out but I think this covers most of the game logic.


Generally speaking, you need to think of your GameObjects as just data and not as unique individuals. As you advance, you'll create fewer classes that generically apply to more and more objects, the bottom line being the minimization of transfers between the CPU and GPU.

  1. Beginner programmers tend to create "objects" for everything and operate the GPU like a standard calculator:

    [number][operator][number][get result][operator][number][get result]
    1 + 2 = 3 + 1 =4

  2. Buckets and buckets of objects are all Updated() and Rendered(), every frame.

  3. Eventually, you realize that you've been spreading your data out, storing it here, and there, and anywhere, even though the GPU prefers super-dense, possibly-massive, streams of data.

    • The GPU operates asynchronously and will do what you've told it to while you are telling it what to do next
    • The GPU operates like an RPN calculator; you load data, then issue commands
      [number][number][number][operator][operator]
      1 2 1 + (3 appears) + (4 appears)

    • The instructions between Begin() and End() are batched and sent to the GPU without waiting

    • The [result] of the previous GPU operation remains "current" until you specify otherwise. (i.e. Draw() calls only affect the current RenderTarget and no RenderTarget can be Drawn() to without being, first, made current by binding it)
    • That is to say, that you can issue commands that use [result] now, even though the GPU hasn't processed the command that produces [result]. That's because [result] will have been calculated by the time the command that uses it actually gets executed.
  4. Eventually, you realize the game is nothing but textured quads, which leads to developing generic, reusable, geometry using DrawInstanced(...)
  5. Instancing makes every component in the game representable as:

    struct PerInstance { uint TextureIndex; uint X; uint Y; uint Width; uint Height; }

  6. With the abstracted structure, you are now able to store ALL game objects in a single (1) super-dense, possibly-massive, per-instance buffer, that is always ready to be uploaded.

  7. Each frame can, now, be rendered with a single (1) DrawInstanced(...) call.
  8. Whatever is left of your "objects" are, now, just slightly-specialized wrappers around the same, generic, PerInstance struct<>. Your final classes will be extreeeeemly simple and look very similar to my lists, above.

As an example of when it is preferable to move from the abstract-as-possible approach to the specific-kinds-of-objects approach, consider that 99% of your game is Ball.Tick() and Ball.Render(). Some bricks never move, some bricks move(break/disappear) occasionally, the paddle moves frequently, and Ball always moves. It makes no sense to re-upload the same stationary bricks every time Ball moves. So, we move Ball, and probably Paddle, to their own PerInstance buffer. One tiny buffer for the fast-movers, updated every frame, and one for everything else, updated only when bricks break.

Lastly, it's a truly wonderful feeling to open a project from last year/month/week and LOL at how completely noob you were "back then"; save a copy before you refactor! Also, instead of refactoring, I'd suggest starting over; sort of a... muscle memory.. thing.

\$\endgroup\$
2
  • \$\begingroup\$ THank you very much! In the DX / low level side of things i'm pretty solid, due to my programming background. Architecturally, seems that for game domain too much OOP is bad, and many times procedural or mixed approaches fit best. \$\endgroup\$
    – Hernán
    Commented Feb 27, 2016 at 14:08
  • \$\begingroup\$ I'm using source versioning/control for my project. It's really surprising like you say, looking at code months ago. Seems so primitive. Again, thanks for your help. \$\endgroup\$
    – Hernán
    Commented Feb 27, 2016 at 15:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .