{"id":166,"date":"2024-07-24T20:38:33","date_gmt":"2024-07-24T20:38:33","guid":{"rendered":"https:\/\/shrubbgames.com\/?p=166"},"modified":"2024-07-24T21:11:20","modified_gmt":"2024-07-24T21:11:20","slug":"vv-eekly-update-7-multiplayer-bugs","status":"publish","type":"post","link":"https:\/\/shrubbgames.com\/?p=166","title":{"rendered":"VV Eekly Update #7 \u2013 Multiplayer Bugs"},"content":{"rendered":"\n<p>Welcome back to the weekly VV Eekly Update, posted every VV Ednesday. It was a real honor when the people in charge decided that we would have a whole day every single week dedicated to Vyn and Verdan! I&#8217;m glad you&#8217;re here celebrating with me.<\/p>\n\n\n\n<p>This week, we&#8217;re going to do a real deep dive into some terrible technical headaches. When I first decided on the general direction for Vyn and Verdan, I knew that this particular aspect would be a real challenge, but I naively sallied forth into the great unknown. Now, I emerge from the depths, bearing some tales to share with the tavern crowd&#8230; of dealing with <strong>NETWORKED MULTIPLAYER<\/strong>!!! DUM DUM DUMMM <em>lightning crashes<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Multiplayer<\/h2>\n\n\n\n<p>Networked multiplayer seems to be the great white whale of today&#8217;s indie devs. Godot recently hosted their 2024 community poll, and a whopping ~75% of ~10,000 respondents said that they wanted to use networking in some aspect and 37% said that they wanted to use real-time networking.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"627\" height=\"319\" src=\"https:\/\/shrubbgames.com\/wp-content\/uploads\/2024\/07\/image-9.png\" alt=\"\" class=\"wp-image-174\" srcset=\"https:\/\/shrubbgames.com\/wp-content\/uploads\/2024\/07\/image-9.png 627w, https:\/\/shrubbgames.com\/wp-content\/uploads\/2024\/07\/image-9-300x153.png 300w\" sizes=\"(max-width: 627px) 100vw, 627px\" \/><\/figure>\n\n\n\n<p>That&#8217;s a ton of people! However, while multiplayer may increase a game&#8217;s fun by ten-fold, multiplayer might also increases the difficulty of creating said game by hundred-fold.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A Single Bug<\/h2>\n\n\n\n<p>Let&#8217;s dive into a bug that I&#8217;ve been smashing my head on for a few months.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"538\" src=\"https:\/\/shrubbgames.com\/wp-content\/uploads\/2024\/07\/bee-bug.gif\" alt=\"\" class=\"wp-image-168\"\/><figcaption class=\"wp-element-caption\">Sorry, it&#8217;s rather small.<\/figcaption><\/figure>\n\n\n\n<p>If you look closely, you can see the bug in this gif. The left side is the server hosting the game, and the right side is a client that&#8217;s connected to the server. They should be the same! When the fight starts, there&#8217;s a subtle difference between the two &#8211; some of the bees on the right side don&#8217;t rotate and get angry!<\/p>\n\n\n\n<p>Here&#8217;s some simplified code for how this works.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>enemy.gd\n\nvar active := false\nvar global_id := 0\nvar body\nvar behavior\n\n# These get synced across clients after enemies spawn.\nvar syncing := &#91;\"active\", \"global_id\", \"body\", \"behavior\"]\n\nfunc init():\n    if is_server():\n        global_id = Globals.get_id()\n        set_up_body()\n        set_up_behavior()\n\nfunc anger():\n    active = true\n    set_starting_state()<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>room.gd\n\n# Maps IDs to enemies for each client.\nvar enemies := {}\n\n# Called when a body enters into the room.\nfunc body_entered(body: Body):\n    if body.parent is Enemy:\n        var enemy := body.parent as Enemy\n        enemies&#91;enemy.global_id] = enemy\n\nfunc anger():\n    for enemy : Enemy in enemies:\n        enemy.anger()<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>encounter.gd\n\nfunc start_encounter():\n    room.anger()<\/code><\/pre>\n\n\n\n<p>The whole thing starts by calling <em>Encounter.start_encounter()<\/em>. Take a look, and guess which line the bug is on. I&#8217;ll give you an imaginary cookie if you&#8217;re right.<\/p>\n\n\n\n<p>This bug has been appearing in-game for months now. It&#8217;s not an absolutely terrible bug &#8211; it doesn&#8217;t happen all the time, and I can still test other aspects of the game while this is happening. Some play-testers don&#8217;t even notice when it happens, since it only happens sometimes on rooms with many enemies. The chaos camouflages the fact that some of the enemies aren&#8217;t really doing anything. However, it&#8217;s definitely game-breaking, so I decided to take some time to debug it.<\/p>\n\n\n\n<p>The real difficulty with debugging this is that it only happens <em>sometimes<\/em> when hosting multiple clients in large encounters. I would attempt to reproduce this, and the bug would only appear on the third or fourth attempt. There would need to be at least forty enemies on the screen, and I would have to scroll through all the debug data to see what&#8217;s happening.<\/p>\n\n\n\n<p>Eventually, I pinpointed this to the &#8220;anger&#8221; section. I added these logging statements.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>enemy.gd\n\nfunc anger():\n    <strong>Log.debug(\"Angering enemy \" + name())<\/strong>\n    active = true\n    set_starting_state()<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>room.gd\n\nfunc body_entered(body: Body):\n    if body.parent is Enemy:\n        var enemy := body.parent as Enemy\n        <strong>Log.debug(\"Adding enemy \" + body + \" to room \" + room)<\/strong>\n        enemies&#91;enemy.global_id] = enemy<\/code><\/pre>\n\n\n\n<p>These statements showed that the enemies were being added to the room, but that some of them somehow weren&#8217;t being angered.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Adding enemy Enemy1 to room BossRoom\nAdding enemy Enemy2 to room BossRoom\nAdding enemy Enemy3 to room BossRoom\n&#91;...]\nAdding enemy Enemy39 to room BossRoom\nAdding enemy Enemy40 to room BossRoom\n<strong># Why are enemies 1 through 24 not being angered??\n<\/strong>Angering enemy Enemy25\nAngering enemy Enemy26\nAngering enemy Enemy27\n&#91;...]\nAngering enemy Enemy39\nAngering enemy Enemy40<\/code><\/pre>\n\n\n\n<p>If you want another chance to figure out which line needs fixing&#8230; here&#8217;s your last chance to scroll up and look! Take a good guess!<\/p>\n\n\n\n<p>&#8230;<\/p>\n\n\n\n<p>Here&#8217;s the broken line!<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>room.gd\n\nfunc body_entered(body: Body):\n    if body.parent is Enemy:\n        var enemy := body.parent as Enemy\n<strong>        enemies&#91;enemy.global_id] = enemy  # &lt;-- THIS LINE!<\/strong><\/code><\/pre>\n\n\n\n<p>The global ID is one of the attributes that get synced across clients after enemies spawn. However, it was possible that the body was being added to the room <strong><em>before<\/em><\/strong> the global ID was synced! Here&#8217;s some possible timelines &#8211; the only difference is that the last two lines of each are swapped in order.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>A Good Timeline!\n\n<strong>On the server:<\/strong>\nEnemy30 added to the server. (Server global ID starts at 0)\nServer sets Enemy30's global ID to 30.\nServer adds Enemy30 to the room.\n\n<strong>On the client:<\/strong>\nEnemy30 added to the client. (Client global ID starts at 0)\nServer tells client that Enemy30's global ID is 30!\nClient adds Enemy30 to the room. Everything is good.<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>A Bad Timeline :(\n\n<strong>On the server:<\/strong>\nEnemy30 added to the server. (Server global ID starts at 0)\nServer sets Enemy30's global ID to 30.\nServer adds Enemy30 to the room.\n\n<strong>On the client:<\/strong>\nEnemy30 added to the client. (Client global ID starts at 0)\nClient adds Enemy30 to the room - <strong>but wait, global ID is still 0!<\/strong>\nServer tells client that Enemy30's global ID is 30, <strong>but it's too late!<\/strong><\/code><\/pre>\n\n\n\n<p>This only happens sometimes for some of the enemies, since it depends on whether the client gets around to adding enemies to the room before the global ID is synced. Usually, the global ID is synced quickly, but it sometimes takes more time if there are more enemies in a single room. This explains why it only happens sometimes in large rooms!<\/p>\n\n\n\n<p>The fix here is to wait for the global ID to be set.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>enemy.gd\n\nfunc get_global_id():\n    if global_id == 0:  # A global_id of 0 is invalid.\n        await global_id_set\n    return global_id<\/code><\/pre>\n\n\n\n<p>Wow, that was easy! (not)<\/p>\n\n\n\n<p>Now, imagine having to understand your global server and client state constantly, always having to account for syncing delays, and always remembering which fields are synced and which are not. This requires a discipline that&#8217;s difficult even for experienced backend programmers like myself! There are paradigms that I have found helped me, which I will go over in a future posting, but even those don&#8217;t cover everything. Networked multiplayer is hard!!<\/p>\n\n\n\n<p>Thanks for reading all the way to the end. Please let me know if this post was interesting for you, or if you&#8217;d rather me go back to game design! Any feedback is appreciated, and, as always, can be timely posted into the Discord.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Welcome back to the weekly VV Eekly Update, posted every VV Ednesday. It was a real honor when the people in charge decided that we would have a whole day every single week dedicated to Vyn and Verdan! I&#8217;m glad you&#8217;re here celebrating with me. This week, we&#8217;re going to do a real deep dive [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":73,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_coblocks_attr":"","_coblocks_dimensions":"","_coblocks_responsive_height":"","_coblocks_accordion_ie_support":"","_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[3],"tags":[],"class_list":["post-166","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-vv-eekly-updates"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/shrubbgames.com\/index.php?rest_route=\/wp\/v2\/posts\/166"}],"collection":[{"href":"https:\/\/shrubbgames.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shrubbgames.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shrubbgames.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/shrubbgames.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=166"}],"version-history":[{"count":7,"href":"https:\/\/shrubbgames.com\/index.php?rest_route=\/wp\/v2\/posts\/166\/revisions"}],"predecessor-version":[{"id":175,"href":"https:\/\/shrubbgames.com\/index.php?rest_route=\/wp\/v2\/posts\/166\/revisions\/175"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shrubbgames.com\/index.php?rest_route=\/wp\/v2\/media\/73"}],"wp:attachment":[{"href":"https:\/\/shrubbgames.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=166"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shrubbgames.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=166"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shrubbgames.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=166"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}