Week 7
This week I am finishing up for handing in a binary for IARVR with various small things - coins, stop watch, fixing a grabbing and T-Shape task interaction, hand movement and statistics for playtesting.
Global Signals
I make use of global signals for most of this week's topics. They're very useful. Watch this on how to implement them in Godot.
These global signals behave somewhat like an event queue or observer pattern.1 Robert Nystrom's Game Programming Patterns covers both patterns. In lieu of motivating the patterns myself, I will refer to his motivation sections.
Coins
Coins were a joy to implement. Model a shiny cylinder, add a collider, and even an AnimationPlayer for making the coins enticing to collect. So delightfully simple and self-contained compared to everything else.
Coins are a great example for decoupling game logic with global signals.
In coin.gd I don't particularly care what happens to the money as an abstract payment unit. I do care about making the physical coin disappear when it is collected. On the other hand in inventory.gd I don't particularly care how the player acquired abstract payment units. I care about tallying and exposing interfaces to other systems.
For the current development state, the approach solves the problem of keeping track of many coins collection effects even after they were freed from memory. More interestingly, it offers good extension points for things like coins of different values, value multipliers, spending abstract payment units, receiving quest rewards, etc.
getMoney is used for displaying the current amount of collected coins in a head-up display without coupling coin collection code to the head-up display code.2
Stop Watch
I need a stop watch for statistics. Implementation is boring. Since I already have it, I show it to the player in the head-up display.
Grabbing & T-Shape Task
My grabbing system presented last week has a flaw: The grabbing system is responsible for releasing grabbed objects and it does not expose a way to release an individual grabbed object. This is an issue when I want to move a T-Shape to a new random position during the T-Shape task. The T-Shape is still grabbed after the fact and moved back to the hero's hand.
A good implementation would be to remove CanBeGrabbed from the T-Shape, let CanBeGrabbed release its parent - the T-Shape - in _exit_tree, move the T-Shape and finally attach a new CanBeGrabbed node to the T-Shape.
Instead, because I don't have much time before handing in my binary, I abuse global signals to call release on every CanGrab node. For the current development state this is functionally equivalent to the good solution. Since at anytime there is only ever one grabbable object, releasing all objects is the same as releasing a single object.
Hand Movement
The hero's hand movement shall mirror the player's hand movement. I implement this in two steps. The first is calculating the player's hand position and rotation in relation to their head.
b is the inverse of the head's rotation. Multiplying with it undos the rotation of the head.
The second step is scaling the coordinates in player space to puppet scale.
handScaleFactor's value was determined empirically. As it is a fixed value, it gives and advanatge to people with long arms. During playtesting this was a non-issue, but choosing handScaleFactor should be revisited, if future levels' difficulty depends on arm reach.
It would be possible to simplify hand movement by changing moveHand's signature to and all following alterations. I am only realizing now while writing this blog post!
Statistics
For the survey I want to track a number of things: Time spent walking between start banner, T-Shape tasks and finish banner, time spent on T-Shape tasks, coin collection over time, and position over time. For this I am using the stop watch and global signals mentioned above as well as Godot's FileAccess, which allows for easy writing to a file. Output looks like this:
First is the type of event, then goes time in seconds, and finally in case of type "move" x and z position. The intention is for it to be easy to parse and further process it into nice interactive graphics with D3.js.
Sadly, on the Quest 2 Godot can exclusively write to files, which I cannot read from my computer.3 As a workaround I print to the debug console instead of writing to a file and copy paste the output to a file manually. While this approach, too, works on my computer with a Valve Index, it turns out that printing to the debug console is too unreliable to be usable with the Quest 2. These are the technical issues I mention in the next post.
Footnotes
-
These global signals are not the observer pattern, because they are asynchronous. Neither are they the event queue pattern, because the queue is not exposed to the programmer in any way. Though, I think, it is very likely that Godot signals are implemented using the event queue pattern. ↩
-
Due to a Godot bug, which depends on order of initialization, getting the head-up display to render was painful. I isolated the head-up display UI and rendering into separate scenes so I can get away with not touching the rendering scene until the issue is resolved. ↩
-
This is a known issue. Android storage permissions seem complicated. I am grateful for having an open-source game engine even when it has a few rough edges like this one. ↩