nanoMMO

Drag and drop MMORPG inventory with Dear ImGui

The server sends each player their bag and takes what they do with it, and the client draws it, with icons, tooltips, drag and drop, and the slots that are worn.

Watch on YouTube

We put our MMORPG's inventory on screen. The server sends each player their bag over TCP when it changes, and takes what they do with it, moving, wearing, drinking and throwing away, with a rate limit and refusals in words. The client draws the bag in a window with Dear ImGui and raylib, each item's icon in its tint, with tooltips, drag and drop, and a row in the character window for what's worn, and the windows draw the bag the server sent, so what you see is what the server decided. This is episode 41 of a series about building an MMORPG from scratch in C++, with the focus on the engineering around the game.

The code

Each step on screen is a tag in the game's repository. The code at the end of the episode is ep041/end.

  1. 2:09ep041/step-1Sending the bagBrowseChanges
  2. 9:55ep041/step-2The bag on screenBrowseChanges

Transcript

Intro

0:00 Hello and welcome back to episode forty-one, where we're going to put each player's bag on screen, and let them move, wear and drink what's in it. Let's begin with a quick look at where we're going to be by the end of the episode.

0:13 Each player's bag is on screen now, in a window of its own, with what they wear in the character window, each item's icon in its tint, if it has one. Over an item, a tooltip says what it is and what it does, and a drag moves it, or takes the sword out of alice's hand and into the bag. Then a drag puts the sword back, and the server does each move, and sends the bag back as it changes.

0:36 On the website of the series you can find more information about the project. The code for the project is open source on GitHub, tagged at each step of each episode so that you can follow along.

Last time

0:48 Now, before we get to today's work, let's quickly go over what we did last time. Each player got a bag to carry, and a new player gets the kit in it, once, the sword in hand and three potions. What a player wears adds up to their damage, their health and their armour, and a potion from the bag heals them, with a cooldown of ten seconds. The bags go into Redis when they change, and the saver writes them to MySQL, a row for each stack. Each batch of players is a transaction, so that a bag's old rows aren't deleted without the new ones written.

Today

1:19 Each player has a bag on the server now, which is saved, but they can't see it, and they can't do anything with it. So first, the server is going to send each player their bag, as they join and each time it changes, over TCP, so that it arrives. And it's going to take what a player does with an item: move it, use it or throw it away, with a limit on how fast, and a refusal that says why, in words. Then the client gets a window for the bag, with each item's icon, how many there are, and a tooltip that says what it is. The character window gets a row for what the player wears, and a player drags an item from one slot to another to move it, stack it, wear it or take it off. The client draws the bag the server sent last, so what's on screen is what the server decided. All right, let's get started, with the server.

Sending the bag

2:10 The client is going to tell the server what the player does with their items, so in nanommo dot proto, place moves up, above the input, which is going to use it. A place is where an item is: in a slot of the bag, from zero, or worn, as a one of. Then item move moves some of what's at one place to another: from, to, and how many, which is zero for all of them. Protobuf leaves out a field that's zero, so a move of a whole stack costs nothing for its count. Item action is something a player does with an item, and it's a one of again, as it's one action at a time. A move, or use, with the slot of the bag to use one of, and destroy, with the place to throw away all of what's there. Use takes a slot of the bag, and not a place, as a player uses what's in their bag. The input gets an item action, which is sent once, like a skill. Where the player walks is sent when it changes, and holds until the next input, while an item action is done once, as it arrives. And the world message gets the player's inventory, their bag, and we write how it comes: on its own, over TCP, as you join and as it changes. In server dot h, item handles an item action from a player, next to learn. Send bags sends each player whose bag has changed since we last sent it. The server keeps a token bucket for each player's item actions, like the chat's, and the revision of each player's bag that we sent them last, so that telling whether to send it again takes a number. In server dot c c, the item actions get a rate limit, like the chat's, with a burst of ten, and a refill of one each hundred milliseconds. A drag in the bag is a move, so a player can make a few at once, and then ten a second. When an input has an item action, it goes to item, with the player's id. As the player says hello, they get their bucket, and as they leave, their bucket goes, and so does the revision we sent them. Then item, which does nothing while the server hands its players over to the next instance, and over it, we write why. The next instance loads the bag as it was last saved, so nothing changes it here after that. Then the player's bucket has to have a token, or they're told that they're moving things too fast. The player's world is the copy of the land they're in. A switch on the action's case calls the world for it: move item with the move's from, to and count, use item with the slot, or destroy item with the place. An action with nothing set in it does nothing. A status that isn't okay goes to the player with notify, as a line from nobody, like a trainer's, so the player reads why in the chat. Each tick, after the chat, the server sends the bags. Send bags goes through each copy, and each player in it, and takes their bag's revision. A player with no session, or whose bag is at the revision we sent last, gets nothing, and over it, we write why the bag goes this way. A bag changes now and then, and has to arrive, so it goes on its own, over TCP, when it has changed. Over UDP, a lost bag would leave the client wrong until the next change, and with each tick's world, it would go twenty times a second. Otherwise, a world message gets the player's bag, framed, and its bytes count in the metrics, like the other messages. It goes to the player's session, and the revision is the one we sent. Nothing else goes to the other players, who see what's in a player's main hand once a second, as their weapon. In the client's connection dot h, inventory is what we carry and wear, as the server last sent it, which the connection keeps. In connection dot c c, a world message with an inventory in it is our bag, so the connection keeps it, and doesn't take it for a world. We add the bag to what comes on its own, with nothing of the world in it, with the chat and the moves. The bots get their bag too, and do nothing with it, so in bot dot c c, a world with an inventory returns. We write that our bag comes on its own too, with nothing of the world in it. And in parts dot c c, a player's weapon. Names, levels and weapons come once a second, and in between, protobuf leaves out a field that's zero. So parts kept the last weapon that wasn't zero, and a player who puts their sword away would go on holding it on everyone's screen. A player can hold no weapon now, so a weapon goes with the name. A world with a player's name in it has their weapon too, none included, and a world without the name keeps the last name and the last weapon.

Testing it

6:35 In server test dot c c, inventory dot h comes in, for the places, and test content dot h, for the tests' gear. The test player, which connects to the server like a client, keeps the bag, what the player carries and wears, as the server last sent it. And it keeps the notices, what the server told the player, which nobody said. Send frames an input, with the size in front like any message of ours, and writes it to the server with asio's async write. The bytes go in a shared pointer, which the write's handler keeps, so that they live until the write is done. When the test player reads a message, one that isn't a world is skipped, and over it, we write what comes in worlds of their own, with nobody in them: the chat and the bag. A world with an inventory is the bag, which the test player keeps. Chat with no name is a notice, which goes on the list. And anything else is the world, which is what it kept before. The fixture's tear down takes each player's key off items unsaved too. The new test checks that the server sends each player their bag as it changes. The server from the fixture has no items, so this test makes a server of its own, armed with the tests' gear, which starts a player with the sword and three potions, as its content. Alice connects to it with a token, as a test player, over TLS like a client. She waits until her bag has a revision, which means that it has arrived, and over that, we write what she joins with: the kit, the sword in hand, and three potions. So the sword is in her main hand, and three potions are in her bag's first slot. Over the move, we write that she moves two of the potions to another slot, and the bag comes again. So her input gets an item action, and potions points at its move. It moves from slot zero, made with in bag, to slot four, and its count is two, which leaves one potion behind. A move with a count leaves the rest of the stack where it was, which is the split we tested last time. Then she sends it, as a client would. When her bag has a slot four, it holds two potions, and the first slot has the one that's left. Then a drink, as what she can't do comes back as a line from nobody. She uses a potion from slot four, at full health, and the notice she gets is that she's at full health already. And last, Redis has the bag she has. An H get, in a pipeline of one command, reads the items field of her hash in Redis, which has to work. Its bytes parse as an inventory, and its revision is the one she was sent, so what she sees is what's saved. In parts test dot c c, the test that keeps names and levels between the ticks that send them gets a world more. Over it, we write what it is: a world with their name and no weapon, as they've put it away. So the first player's weapon is zero, as nothing is in their hand.

9:30 The server's test needs MySQL and Redis, so Compose starts them for it, and Docker builds the game and its tests at this step, sped up here. The test passes, with the bag arriving, a move, a refusal and the bag in Redis. Then the protocol's tests, for the parts, which need nothing else, so they run with dash dash no deps. They pass too, with the player who's put their weapon away.

The bag on screen

9:55 The client is going to draw the bag, so it needs two things that the server's inventory kept: how many slots a bag has, and how to make a place. In the content's items dot h, which the client reads too, k bag slots is how many slots each player's bag has, twenty-four. Then the protocol gets a header of its own, places dot h, for the two functions that make places, which were the inventory's. The client and the server both link the protocol, so both of them can make a place, and the header has the guard that each of our headers has. It includes c standard int, the items' generated header, for the slots, and the protocol's, for the place. In bag is a place in the bag, by its slot from zero, and worn is a slot that's worn. In places dot c c, its header comes first, then c standard int, and the two definitions go in our namespace. Each of them makes a place, as they did in the server, and sets the one field of its one of that's theirs. In bag calls set bag with a slot of the bag, and worn calls set worn with a slot that's worn. The protocol's CMake lists builds places dot c c into the protocol's library, next to parts. In inventory dot h, places dot h comes in, so that the server's code that made places with in bag and worn from here still has them. The inventory's k bag slots is the content's now, and the two declarations go. And in inventory dot c c, their definitions go, as the protocol has them.

11:26 Now the window for the bag, in a file of its own, bag window dot h, in the client. It includes c standard int, optional for what a slot returns, string, utility for standard move, and vector for a tooltip's lines. Then the content's items dot h, for the item table, and the protocol, for the inventory and the item action. Item lines is what a tooltip says of a stack, as a list of lines. We write what that is: its item's name, where it's worn, what it adds, what this one rolled, what using one does, and how many stack. It's a function of its own, apart from the drawing, so that a test can check what a tooltip says without ImGui. First empty slot is the first slot of the bag with nothing in it, if it has one, for taking something off. Quick action is what double-clicking the stack at a place does. In the bag, it uses it, if it's used, or else wears it, if it's worn, and on a slot that's worn, it takes it off, into the first empty slot of the bag. It returns an item action, or nothing, for an empty slot, or an item that can't do any of them. Item slot draws a slot of the bag, or a slot that's worn, as a button of the size it's given, so that the bag's window and the character window can share it. Over it, we write what it draws and what a player can do with it. It's the item's icon in its tint, if it has one, with how many there are. A player drags an item from one slot onto another to move it, wear it or take it off, with Shift for half the stack. They double-click it to use, wear or take it off, and right-click it for a menu that throws it away too. It returns what they ask for, which the server does or refuses. Then bag window, the bag, in a window of its own, which I or View, Bag opens and closes. Its constructor is explicit, and takes the item table by value, which it moves into its own. Mutable visible hands the menu a pointer to whether it's open, and toggle opens or closes it. Build draws it, from the bag the server sent, and returns what the player asks for, if anything. It keeps the items and whether it's visible, closed to begin with. In bag window dot c c, its header comes first, then imgui dot h, then c standard def, for size t, c standard int and c string, for mem copy, and optional, string, utility and vector. Then str cat, our icon dot h, which turns a Font Awesome code point into text for ImGui, and the protocol's places. In the unnamed namespace, k payload names what a drag carries, so that a slot takes a drop of an item and nothing else. An icon is twenty-four points, a slot of the bag forty-eight, and the bag has six columns. The count gets a shadow, in black, and a near white. And k unknown icon is Font Awesome's circle question, for an item that this client doesn't have, from an items file newer than its own. Dragged is where a dragged item comes from, and over it, we write why it's a struct of ours. ImGui copies a payload byte for byte, so it's plain data, not the place message, which isn't safe to copy that way, as each protobuf message carries a pointer to protobuf's own bookkeeping. So it's whether it's worn, and the slot, which place of turns back into a place. Key is a place's slot, the bag's or the worn one's, as a number. A helper checks that two places are one place, of one kind, with one key. Stack at finds what's at a place in the bag the server sent, in the bag's map or the worn one, or a null pointer. Find looks an item up in the table, or a null pointer for one this client doesn't know. Stat name is a stat in words, for a tooltip, with a switch over the stat, and a case each for damage, health and armour. The default is the word nothing, for a stat this client doesn't know. Added writes a modifier as its amount with a sign, and the stat's name, plus three damage, say. Slot icon is the icon of a slot that's worn, as an empty slot that's worn shows what goes in it, greyed out. So the main hand is a fist, the off hand a shield, the head a hat, the body a shirt, and the feet socks, each with its name in a comment, as the code point alone says nothing. Color of turns a tint into ImGui's colour, a vector of four floats, and we write that the content's colours are written red, green, blue and alpha, a byte each. So each byte is shifted down to the bottom, masked with two hundred and fifty-five, and divided by it, which makes a float from zero to one, as ImGui has its colours. The last helper in the unnamed namespace is the one that the drags, the double clicks and the menu share. Move to makes an item action that moves the count, all of it by default, from one place to another. It copies the two places into the action's move and sets its count. Item lines starts with an item the client doesn't know, which gets a single line that says so. Otherwise the first line is its name, from the items file. An item that's worn says where, with slot name, and each modifier that wearing it adds is a line. Each roll that this stack has of its own is a line too, with rolled after it, so that a player can tell one blade from another. An item that's used says what it heals, and its cooldown, when it has one, in whole seconds from its milliseconds, with str append onto the line. And one that stacks says how many go in a stack. First empty slot goes through the bag's slots and returns the first that the bag's map doesn't contain, or standard null opt when the bag is full. Quick action finds the stack at the place, with stack at, and its item, with find. It does nothing for an empty place, or for an item it doesn't know. On a slot that's worn, it moves the item into the first empty slot of the bag, and with the bag full, it does nothing. In the bag, an item that's used is used, from its slot, and one that's worn moves into its slot. Anything else, such as a bone, does nothing on a double click. Then item slot, the longest function in the file, as it draws the slot and handles its drag, its drop, the double click, the tooltip and the menu. It finds the stack and its item, like quick action. It starts with no action, which is what it returns when the player does nothing with the slot on this frame. ImGui tells widgets apart by their labels and the IDs pushed above them, and two slots can have one label, such as two empty ones, or two stacks of potions. So we push an ID for each, the bag's slot, or a hundred and the worn slot, which keeps the worn ones clear of the bag's if a window draws both. The icon's colour starts as the text's, with the unknown icon. An item we know has its own icon, in its tint when it has one. An empty slot has no icon in the bag, and a worn one shows its slot's icon, both in the colour of disabled text. The colour goes on the text, and the font is pushed at the icon's size, so the button draws the icon as its label, big enough to read. After the two hashes, the rest of a label is part of its ID and isn't drawn, so the label is the icon, then two hashes and the word slot. Then we pop the font and the colour, so they don't reach anything after the button. A stack of more than one shows how many, in the button's bottom right corner, and the number is the stack's count, as text, with str cat. Get item rect max is that corner, and calc text size is how big the number is. A window's draw list takes shapes and text at points on the screen, and draws them with the window, under anything drawn after. The window's draw list draws it, once a pixel down and to the right in black, as a shadow, and then in near white over it, so that it reads over any icon. A slot with something in it can be dragged. Begin drag drop source starts a drag as the mouse moves with the button down, and the payload is a dragged, of where it's from. While it's dragged, the item's name goes with the pointer. And any slot is a target, empty or not. When an item is dropped on it, accept drag drop payload hands us the bytes of a payload with our name, k payload. Mem copy puts them back into a dragged, and place of turns it into the place it came from. Then moving is the stack at that place, in the bag the server sent. Over it, we write that with Shift, half the stack goes, and the rest stays. So with Shift held, half of a stack of more than one moves. Otherwise the count is zero, which is all of it. A drop on the place it came from does nothing, and any other is a move, from there to here. A double click on a slot with something in it is its quick action. While the mouse is over a slot, with nothing being dragged and the menu closed, a tooltip says what's there. An empty slot is greyed out: empty in the bag, or the slot's name for one that's worn. A stack's lines come from item lines, with the name as the first line in plain text, and the rest greyed out under it. A right click on a slot with something in it opens a menu, and menu item returns true on the frame its item is clicked. Use is there for an item in the bag that's used, wear for one that's worn, and each returns its action. On a slot that's worn, take off moves the item into the first empty slot. Over it, we write that it's greyed out when the bag has no room for it, which the menu item's last argument does. And throw away is an action of its own, destroy, on the place, for any item. Then we pop the ID we pushed, and return what the player asked for, or nothing. Bag window's build draws nothing while it's closed. It takes a margin of ten points, and the size of the screen, from ImGui's display size. The first time it opens, it goes at the right edge of the screen, ten points in, with the middle of its height at the middle of the screen's. A pivot of one across and a half down puts the window's right edge and the middle of its height on that point. After that, the player can move it, as first use ever leaves the window where they put it. The window is called Bag, its close button closes it, and it fits its size to the slots, without the arrow that would collapse it to its title bar. Then the slots, twenty-four of them, with same line between them but at the start of each row of six. Each slot is an item slot, at forty-eight points, and what a player does in any of them is what the window returns. In the client's CMake lists, the client's library builds bag window dot c c, and the client's tests get a file of their own for it. They read the game's own items, so they get the content's folder, as a definition, NANOMMO CONTENT.

22:10 What a player wears goes in the character window, which since episode seventeen has their picture, their level, their health and where they are. In character screen dot h, optional and utility come in, and the content's items, as the window draws items now. Its constructor takes the item table, and it's explicit, like the bag window's. Build takes the player's bag, and returns an item action, if any. Over it, we write that it draws the window, while it's open, with what the player wears, and returns what they do with it, as the bag's window does. The window keeps the items it's given, in items. In character screen dot c c, optional, utility, our bag window dot h, for item slot, and the protocol's places come in. A worn slot is forty-four points, a little smaller than a slot of the bag, so that a row of five fits beside the picture. Build returns an optional item action now, in place of nothing, so that what the player does in the row of worn slots gets back to main, and it takes the bag. It returns nothing while the window is closed, or before the player has a level, as we know nothing of them yet. The action that it returns starts empty, for no action. The window is forty points wider, for the row of five slots. After where they are, a separator, and the word worn, greyed out. Then a slot for each place that's worn, in order: the main hand, the off hand, the head, the body and the feet, on one row, with same line before each but the first. Each is an item slot with the place that's worn, so dragging, dropping and the tooltips work as they do in the bag, and dragging between the two windows works too. At the end, build returns what the player asked for. In menu dot h, build takes a pointer to whether the bag is shown. And in menu dot c c, the view menu gets bag, with Font Awesome's shopping bag and the key I next to it, like character and C. In characters dot c c, which draws the players and what they hold, the held item gets its tint. When its look has a tint, color tint multiplies the model's colour by it, and we write why: a tint makes another item of a model, such as the ash blade of the sword. So the ash blade is the sword's model, a little longer, in an ember orange. In main dot c c, bag window dot h comes in. The character screen gets the content's items, and so does the bag window, next to it. I toggles the bag's window, like C toggles the character window, unless the player is typing in the chat. The character window builds with the bag the connection keeps, the one the server sent last, and returns what the player asked for in it. Over it, we write where that goes: what the player does with their items, in either window, goes with what we sent last, like a line of chat. The bag's window builds with that bag too, and what's asked for in it takes the place of the character window's. When there's an item action, we send an input that's a copy of the one we sent last, with the item action in it. That way, the player keeps walking as they were, and the server takes the action once. The menu gets whether the bag is shown, so that the view menu can open and close it.

Testing the windows

25:19 The client's tests, in bag window test dot c c, check the parts of the bag's window that aren't drawing: what a tooltip says, what a double click asks for, and the first empty slot. It includes c standard int, optional, string and vector, the content's header, for reading the items, GoogleTest and the protocol's places. Items reads the game's own items, as the client reads them, from the content's folder that CMake gives the tests. Of makes a stack of an item, one by default. And four of the game's items get names: the sword is one, the potion two, the tunic five and the ash blade six. The first test says what an item is and what it adds. It makes an ash blade, item six, and adds a roll to its stack, of three damage, as if this one had rolled it. That blade reads ash blade, worn in the main hand, and three damage, rolled. The line for the roll ends with rolled, so that a player can tell this blade from another. The tunic reads leather tunic, worn on the body, with one armour and ten health, its two modifiers, in the order of the items file. Three potions read health potion, heals forty, one each ten seconds, and stacks up to ten. And item ninety-nine, which the items file doesn't have, reads that it's an item this client doesn't know. The second test is a double click, which uses, wears or takes off. The bag has three potions in its first slot, the tunic in its second, and the sword in the main hand. A double click on the potions asks to use slot zero. Each check asserts that there's an action, and that it's the right kind, before reading it. One on the tunic is a move, into the body's slot, as that's where a tunic is worn. One on the sword in the main hand is a move too, and over it, we write where to: off, into the first slot with nothing in it, which is slot two. And a double click on slot five, which is empty, asks for nothing, so quick action returns an empty optional. The last test finds the first empty slot, in a bag that fills up. An empty bag's first empty slot is zero. Then we fill each slot but seven, with a potion, and seven is the first empty one. With seven filled too, the bag is full, and there's no empty slot, which is standard null opt.

27:38 Docker builds the game and its tests at this step, and the client's tests need nothing else, so they run with dash dash no deps. And grep keeps the lines of the tests that pass, out of the build's output and GoogleTest's own. All three pass, the tooltips, the double click and the first empty slot.

Opening the bag

27:58 So let's log in as a new alice, and press I, which opens the bag's window at the right of the screen, with the three potions of her kit in the first slot. Over them, the tooltip says that it's a health potion, which heals forty, one each ten seconds, and that they stack up to ten. Over one of the other slots, the tooltip says empty, greyed out. Then C opens the character window, with the new row of what she wears. The sword is in her main hand, and its tooltip says that it's worn in the main hand. The other slots are greyed out, each with an icon for what goes in it, like the shield of the off hand.

28:33 With both windows open, let's drag the potions from the first slot into another, and the server moves them, and sends the bag back, so they're in their new slot. Then the sword, from her main hand into the bag, and her hand is empty, in the window and in the world. We drag it back onto the main hand, and she's holding it again. Now the potions onto the off hand, which takes what's worn there, such as a buckler. The server refuses it, and the chat says why: she can't wear the health potion. And to see that the bag is saved, she logs out, from the game menu, and logs in again, with her name still filled in. Her bag comes back as she left it, with the potions in their new slot and the sword in her hand.

Wrap-up

29:17 So, today the server sends each player their bag over TCP, as they join and each time it changes, and nobody else gets it. It takes what they do with an item, a move, a use or throwing it away, ten at once and then ten a second, and a refusal comes back in words, in the chat. The client draws the bag in a window of its own, each item's icon in its tint, if it has one, with how many there are, and a tooltip that says what it is. The character window has a row for what's worn, and a drag moves an item, stacks it, wears it or takes it off, with the server deciding. And the bag comes back as it was left, from the saving of last time.

Playing it

30:45 Let me know in the comments what you'd like to see next. And if you want to follow along with the series, subscribe to the channel. I hope you find this useful, and thanks for watching.