Today is just the first in a series of brief game development updates giving a quick glance at some of the features I'm currently working on in HeroBound. This week has been mostly polishing the in-game clock and visual day/night cycles, adding chat commands, and re-adding shadows. So let's get to it.
Shadows have been in the game for a while, but even low-quality shadows became a significant hit on performance. On my The beefed up Windows 10 machine that I rarely use these days.Specs For Nerds
Instead of baking the shadows, I opted to see if there wasn't a way to improve the performance (at the cost of quality) of real-time dynamic shadows. And sure enough, my first attempt worked quite nicely. All I did was disable the default rendering of the shadow generator, and manually update the shadows in a sub-render tick that caps at 10FPS, which is far more than enough for the shadows of static objects, given the in-game sun rises and sets (180deg movement) over 80 minutes of real time. Honestly 1FPS would be plenty for the shadows to shrink and grow smoothly. The issue is that this also affects moving objects, so having your shadow lag behind you and only update once per second is not a good experience. At 10FPS, however, it doesn't look too terrible.
Next, what I attempted to do was create two shadow generators: one for static objects that can be updated around 1Hz, and another for dynamic/moving objects to be updated in the normal render loop. This proved to be unsuccessful. I believe there is way to do this, but so far my attempts resulted in no shadows at all, so I will have to revisit this another time. For now, I can live with the choppy shadow movement if it means I can still maintain 120FPS (max) when playing the game on my The 16-inch MacBook Pro I use for development.Specs For Nerds
During the shadow-testing, I realized there was a time-sync issue on the client, because I was adding the deltaTime
to the in-game time on every frame. The problem, as I recently realized, is that there is a time drift because the highest resolution timestamp you can get in the browser is milliseconds, which means anything higher is truncated. I was under the impression that performance.now
was truncated on all browsers to fight fingerprinting, but on most browsers, you can disable the clamp on this, which is essential for rendering properly.
This lead to a full refactor of the in-game clocks on both client and server, and now, not only is the in-game time perfectly synced between browsers and clients, but in the process I was also able to improve the client-side state interpolation. Since every interaction is server-driven (clients wait to do anything until the server sends an update), properly translating server timestamps to the local render cycle is essential for smooth interpolation, and it's not very complex at all. The basic formula is clientOffset = serverTimestampFromInitResponse - currentClientTimestamp - avgServerTick + initRTT / 2
. Then Date.now() + clientOffset
will result in a very close approximation of the server time that is currently being rendered.
RTT (Round Trip-Time) is the time it takes for a message to go from the client, to the server, and back to the client. When games show your
Ping
, it's usually just calculated as half the RTT. In this case, we are estimating the time lost between the server and client reading the current time to adjust the differences in system clocks.
After refactoring and improving the shared game clock, I decided it was time to add a system by which I can change server vars (such as the current game date/time) using chat commands. Here's what that looks like:
Having a chat-command system in place, I couldn't really help myself from adding emotes.
There will eventually be an in-game menu for emotes, but this will suffice for now. The syntax is /<emote> [@<beggining name of nearby player> | @<exact name of nearby player>!]
(e.g., /wave
, /wave @zimmed!
, /wave @zim
). When targeting a player, it will search the surrounding 3x3 grid sectors for a match. If more than one target is found (and the optional bang !
is not used) the match will fail -- which is necessary in case you have two players named JimBob
and JimBob2
and you target @jimbob
. In most cases, you will just need to do it again with a more specific query, but in the previous example, you will need to use @jimbob!
to target JimBob
. The query also matches from the beginning of the name, so you can't use @bob
to target JimBob
, but you can use @jim
(as long as @jim
doesn't have any other nearby matches). You can also just use the emote without a target, or target yourself and look like an idiot!
• • •
That's it for now. If you have any questions or comments, reach out.
zimmed
Last updated 2 days ago.