How to handle building placement logic & rendering in an RTS?

Started by
4 comments, last by Zipster 4 years, 7 months ago

Hi all,

I'm making a little prototype for a tile-based RTS just for fun and I'm having some problems structuring the UI side of things.

There's several problems in my head at the moment, but I'll avoid putting them all into one post for now.

I'm using an ECS, and so my game logic is separate from my rendering logic (I have a RenderSystem which draws the map). The render system has the camera for converting from world->screen coordinates and knows which sprite to draw for each tile. The render system also has a UI (a sidebar like the C&C games etc). This consists of the traditional tree structure of View classes like Buttons, TextViews etc. When the user selects a building (via a button) from the sidebar, with a view to placing it on the map, I'm not sure how to handle this.

Showing a preview of the building at the position of the mouse cursor (with green tiles for valid placement, red for invalid) requires knowledge that only the logic can know (which tiles can this type of building be placed on, has the player got enough money etc).

How best for the render system to retrieve this information?

Any thought welcome, thanks in advance to any who reply.

Advertisement

There should be a layered organization : the high-level GUI controller (the component that decides, for example, that since the mouse cursor has been hovering approximately in the same place for enough time, a preview of possible buildings at cursor should be drawn) can ask a service about game state and game rules questions like "what map tile is below this point" (corresponding to the mouse cursor, transformed toworld-space coordinates) and "what buildings can be built at this map tile". Then a proper low-level GUI module can draw preview animations of those buildings.

Omae Wa Mou Shindeiru

Most, if not all of what you're saying should be handled by the logic thread. In fact, the render thread is the least relevant one here (for the most part). For UI actions what you're concerned about are the input and logic threads.

  • Let's say there's some mouse input (a click, mouse move event or whatnot).
  • The input thread pushes this in a queue.
  • During the next tick the logic thread processes the input queue and determines what the result of the input is...
    • ... If the mouse is over a "Build Wind Trap" button, and the input is a click it triggers the Build Building mode with the Wind Trap as the argument (see below).
    • ... If the game is already in Build Building state, it sets a sate for the render thread ("draw this building here, please"). If the mouse has moved, it first performs a raycast to get the position on the ground*. Finally, it performs any suitability checks for the building placement. If the placement is good, it sets another part of the state for the render thread - "draw it in green please" (or "red, please" otherwise).
    • ... If the user is hovering over a valid location and performs a click, it adds the building to the game world during that tick.

* Now, hit testing can be just a smidge trickier since you can perform this on the logic/physics thread or during the render pass as feedback from the GPU. In the latter case you use a similar mechanism to propagate the current world (or view) space coordinate back to the logic thread, which then ties the two things (input and render thread) together.

>> How best for the render system to retrieve this information?

Create a simple struct that relays this information. Make sure only one thread can write to it and you'll be free of most multithreading issues, though proper synchronization and paying attention to who owns the data at any given time goes a long way.

Consider really simple communication between the input and logic threads:

struct mouseinput_t {
  int button;
  int state;
};

struct inputqueue_t {
  std::mutex _mtx;
  std::vector<mouseinput_t> mouseInput;
};

Write to the queue (input thread):

mouseinput_t newMouseEvent;
newMouseEvent.button = MOUSE_LEFT;
newMouseEvent.state = MOUSE_DOWN;

queue._mtx.lock();
queue.push_back(newMouseEvent);
queue._mtx.unlock();

Read from queue (logic thread):

queue._mtx.lock()
for(auto event : queue.mouseInput) HandleMouseEvent(e);
queue.clear();  // you'll want to do this here
queue._mtx.unlock();

If your input processing is very heavy, then you'll want to make a local copy of the queue, as shown below.

 

Communication between the logic and render thread is similar:

struct preview_t {
  std::mutex _mtx;
  const builting_t* building;
  color_t color;
  vec3_t position;
};

Logic thread:


preview._mtx.lock();
preview.color = COLOR_GREEN;
preview.position = hitTestResult; // you got this from the raycast
preview.building = buildingType_WindTrap; // some object that describes the Wind Trap building
preview._mtx.unlock();

Render thread:

preview._mtx.lock();
auto localPreview = preview; // make a copy of the state since the render thread likely runs a fair bit faster than the logic thread and can stall the logic thread unnecessarily
preview._mtx.unlock();

if(localPreview.building) DrawBuildingPreview(*localPreview.building, localBuilding.position, localBuilding.color);

 

 

Thank you both for the replies and for going to so much effort regarding the code snippets etc, really appreciate it.

I like the idea of the back-and-forth to keep the building logic away from the render layer. My camera is currently in the render layer at the moment and thus ATM the logic layer wouldn't know how to convert the mouse position to a world position. I'll have a think about either moving the camera into the logic layer (maybe by becoming an actor like other game entities) or maybe I could abstract the mouse events in the render layer into build/preview requests received by the logic layer.

Along the lines of what @LorenzoGatti mentioned, I would create a separate service that can be queried for tile state and other building placement information, which can be shared by both the logical and rendering systems. This will minimize any duplication of logic and allow everything to function uniformly.

Multi-threading wasn't mentioned in the original topic, but even then it's easy to handle by giving the render thread its own copy of the relevant data which is updated after every logic frame. You don't even need to expose anything about this data, it can just be an opaque handle that the render thread (and logic threads) can pass to the service along with requests.

This topic is closed to new replies.

Advertisement