Graphical user interfaces in a multithreaded environment

Started by
1 comment, last by haegarr 10 years, 4 months ago

So I have two main threads in my game. One managing the window and rendering, and another thread doing all the logic.

The logic thread is then filling out state buffers with relevant bits of the world data that the render thread can then render (interpolating between the two most recent buffers). The window thread forwards the keyboard and mouse input events to the logic thread to keep player movement and such simple.

My current idea is to follow the same system as the main game world, and keep all interactions on the logic thread, while telling the render thread what it needs to render. However a solution that lets all the ingame UI stuff not care about threads so much would be nice.

I am also thinking about breaking some of the logic stuff out more (e.g. clickedOnSlot and the functions it works with) to help with working on multiplayer, but that still seems to leave me with this problem where that stuff is on a different thread to the actual rendering, even if I moved the UI's user input events back to the render thread...


    /**@brief Abstract class for interacting with and rendering an inventory.*/
    class BaseInventoryUi
    {
    public:
        /**Will be constructed on the logic thread when the player does
         * something that needs a UI.
         */
        BaseInventoryUi(
            grf::HudRenderer &renderer,
            Inventory &inventory);
        virtual ~BaseInventoryUi(){}

        /**Must be called on the render thread!*/
        void init();

        void render(int stateIndex);
        /**Must be called by the logic thread since it access the inventory
         * reference.
         */
        void logicUpdate(int stateIndex);

        /**Must be called on the logic thread!*/
        void onMouseDown(const MouseEvent &evt);
        /**Called by render thread to handle tool tips*/
        ItemStack getRenderStackAt(int x, int y, int stateIndex)const;
    protected:
        Inventory &inventory;
        const unsigned size;

        virtual Vector2I getSlotPos(unsigned slot)const=0;
        virtual int getSlotAt(int x, int y)const=0;
        virtual ItemStack getHeldStack()const=0;
        virtual void setHeldStack(ItemStack itemStack)=0;
        /**In UI's screens with 2 inventories, move this stack to the other
         * inventory.
         * @return The number of units that could not be moved.
         */
        virtual uint16_t moveToOtherInventory(unsigned from, ItemStack itemStack)=0;
    private:
        ItemSlotsRenderer itemSlotsRenderer;

        //A better way?
        struct State
        {
            Inventory inventory;
        };
        State states[3];

        /**Must be called on the logic thread!
         * - Shift click to move stack to other inventory
         * - Left click to place held stack, and pick up existing stack if different
         * - Right click when holding nothing to pick up half the stack
         * - Right click when holding something to place 1
         * 
         * Calls:
         * - Any methods using the this->inventory reference
         * - getHeldStack
         * - setHeldStack
         * - moveToOtherInventory
         */
        void clickedOnSlot(
            unsigned slot,
            bool left,
            bool shift);
    };

#include "Precompiled.hpp"
#include "BaseInventoryUi.hpp"
#include "../ItemStack.hpp"
#include "../Items.hpp"
namespace game
{
BaseInventoryUi::BaseInventoryUi(
        grf::HudRenderer &renderer,
        Inventory &inventory)
    : inventory(inventory)
    , size((unsigned)inventory.getSize())
    , itemSlotsRenderer(renderer)
{

}

void BaseInventoryUi::init()
{
    std::vector<Vector2I> slots;
    for (unsigned i = 0; i < size; ++i)
    {
        slots.push_back(getSlotPos(i));
    }
    itemSlotsRenderer.init(slots);
}

void BaseInventoryUi::render(int stateIndex)
{
    const auto& state = states[stateIndex];
    itemSlotsRenderer.renderItems(state.inventory);
}
void BaseInventoryUi::logicUpdate(int stateIndex)
{
    states[stateIndex].inventory = inventory;
}

void BaseInventoryUi::onMouseDown(const MouseEvent &evt)
{
    int slot = getSlotAt(evt.pos.x, evt.pos.y);
    if (slot < 0) return;
    //TODO: Multiplayer?
    clickedOnSlot((unsigned)slot, evt.button == VK_LBUTTON, evt.shiftDown);
}


void BaseInventoryUi::clickedOnSlot(
    unsigned slot,
    bool left,
    bool shift)
{
    ...logic...
}
}
Advertisement

Threads can access the same memory space, you should probably have an object that contains the information of the inventory logic visible to both threads.

For instance, in the logic object you have a list of items (also objects), each item have the method getGraphic, that returns the png path of that object. So when the user consumes an item, the logic thread removes it from the array, the drawing thread then notices the difference and updates the inventory. To use this model you will need to implement a mutex so that the rendering thread won't read/write at the same time of the logic thread.

Personally I think that this a bad design, logic tends to be thousands of times faster than the rendering and they are related (so you will end up waiting in the lock a lot), in other words the gain would be very small, unless you have something really heavy going on in the logic thread.

Currently working on a scene editor for ORX (http://orx-project.org), using kivy (http://kivy.org).

A game is rendered constantly and high frequently, opposed to a desktop application where rendering is triggered by events. The rendering shows a current state of the data model. There is no need for the renderer to ever see user input. It just sees the result of what user input (or something else) has done to the data model.

The distinction between the three components of the MVC pattern is very well suited here. There is a world model. There is a model for inventory. There is a view able to render the world model. There is a view that is able to render the inventory model. There is a controller that gets user input (letting aside whether it is "physical" or "logical" input) that applies changes to the models.

The main controller gets input "open player inventory". It locates the belonging data model, installs the inventory view as an overlay with the inventory data as model to be rendered, and installs an inventory controller (with self as parent and the inventory as model) that is further responsible for user input.

This leads naturally to a separation of things: Don't use a single UI object but a couple of objects working together.

This topic is closed to new replies.

Advertisement