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);