It's 3:14pm EDT! Happy Pi Day again!
#PiDay
Posts by Checkpoint Earth
Celebrate #PiDay with the Infinite McConaughey Livestream event. Starts today at 3:14 PM ET.
It's π Day! To celebrate this enigma, who is also the player character in our game, here's a gameplay video that demonstrates some of the sounds crafted by @craveninouterspace.bsky.social Watch it unmuted. If you're wondering what's going on, you'll enjoy the game!
#PiDay #IndieGames #GameDev
That voice was telling me that people buy all sorts of things and to keep working. So that's what I'm doing today (no, my game does not contain sex)
On my vision quest, the one that led me to the game I'm working on today, the question that kept coming up was: "why would anyone buy this game?" The immediate and crude response I got was: "people pay to watch other people having sex". (it used a different word for "having sex")
Here's a sneak preview of one of the challenges in AGENCY. If you're wondering what's going on, you'll have a great time in the game! Shout out to @craveninouterspace.bsky.social for the idea and the music! (Watch it with your audio turned on.)
#indiegames #indiedev
So I wrote that thread on hunting a bug in our game. I was initially thinking of writing a blogpost, maybe even make a video. but then this happened lol
#indiegames #indiedev #indiegamedev #tuesdaynegotiations
I now have to go through my entire project and make sure the pointers are all defined and used properly. Save me, ripgrep. You're my only hope!
If you have a correction to make or have questions, please share them!
/end
#Unreal #GameDev #IndieGameDev #BugHunting
That said, I am aware that there are other things I don't know. So even though I can no longer reproduce this issue with the fix, I am open to the possibility that I get a crash in the future with similar symptoms. Well, that investigation will happen when it needs to.
23/
Sometimes, one has no choice but to implement a known solution to fix a problem and move on to other things. But when it's possible, it's nice to take the time to really understand the bug. It's fun to learn the internals of something.
22/
So with this fix, "IsValid()" should work properly because the underlying memory will not be re-used until I no longer have access to it myself. I could go one step further here and use TWeakObjectPtr, which will get the GC to perform pointer nullification itself if the object is destroyed.
21/
The solution is to change how the variable is defined:
UPROPERTY()
TObjectPtr<ASomeObject> CurrentInteractable;
UPROPERTY is a C++ macro specific to Unreal. In conjunction with TObjectPtr, it prevents the GC from reusing the memory until I manually set the variable to "nullptr" myself.
20/
In the previous screenshot, who knows what that area of memory "CurrentInteractable" points to and what it's being reused for. As far as "IsValid()" is concerned, the "object" is valid but it's just some random area in memory and doing anything on it cannot lead to anything good.
19/
A screenshot from the debugger that shows what "CurrentInteractable" looks like in memory the bug happens. The ObjectFlags do not include "RF_MirroredGarbage", so "IsValid()" thinks the object is valid.
I needed to verify this so I added some breakpoints and tried reproducing the bug. Here's a screenshot showing the "CurrentInteractable" variable when the bug happens. The "ObjectFlags" area do not include any garbage flags, so "IsValid()" returns true here.
18/
So if the object was destroyed prior to running this code, "CurrentInteractable" becomes a pointer address pointing to _some_ area in memory. "IsValid()" will interpret data in that area in memory but its answers shouldn't be expected to be coherent. Who knows what data it's looking at.
17/
Well, it turns out that when the GC cleans up the object, it doesn't necessarily return the memory used by the object back to the OS. The memory allocator might reuse the returned memory for a different object without involving the OS. Of course! That's a classic thing to do for performance.
16/
Maybe the object was destroyed prior to running this code. But why would "IsValid(CurrentInteractable)" return true, though? Shouldn't it have seen the "Garbage" flag set on the object and returned false, instead?
15/
A screenshot from the debugger that shows what "CurrentInteractable" looks like in memory when the garbage flags are set on it before the object is cleaned up. The ObjectFlags includes "RF_MirroredGarbage", which is what "IsValid()" checks for.
Here's a view from the debugger when the object actually has the garbage flags set on it but before the object is cleaned up by the GC. Notice the flag "RF_MirroredGarbage". The "IsValid()" function checks for this. FYI, I censored parts of the screenshot to not spoil the game :)
14/
The other possibility was that "IsValid()"'s results are unreliable when used with a raw pointer. But that makes no sense. IsValid()'s implementation is simple: check if the pointer is not a nullptr and check that the object doesn't have a "Garbage" flag set on it.
13/
If nothing else can preempt my code to clean the object, and if the garbage collector is not running in parallel to clean the destroyed object, then if "IsValid()" says the object is valid, shouldn't the object remain valid within the if statement block?
12/
This had the appearance of a race condition. Something between triggering the event, destroying the actor, and Unreal's garbage collector. But why? I used "IsValid()", didn't I? Unreal's garbage collector (GC) is supposed to be synchronous, at least the part that cleans up an object.
11/
This latter declaration is safer if you're referring to objects whose lifecycle is managed by the engine. I should have written it that way to begin with but this is an older piece of code, written when I was less experienced with the engine's internals. But anyway, why did it crash?
10/
I should point out that "CurrentInteractable" is an instance variable and it's a raw pointer. It's something like this:
ASomeObject* CurrentInteractable;
In Unreal, it's better to do something like:
UPROPERTY()
TObjectPtr<ASomeObject> CurrentInteractable;
9/
The issue is this: despite "IsValid()" returning true, the call to "Execute_EndInteract" fails and the stack trace usually is something weird. It felt like the object was actually a garbage object inside the if statement block. So why did "IsValid()" return true?
8/
A quick explanation: "IsValid()" checks if the variable "CurrentInteractable" is a pointer to a valid object that's not being garbage collected. If that's true, the interface function "EndInteract" is called on "CurrentInteractable", which implements the interface, before being set to null.
7/
This time, I had more success reproducing the issue. It turned out to be this section of code:
if (IsValid(CurrentInteractable))
{
IWInteractable::Execute_EndInteract(CurrentInteractable, GetOwner(), false);
CurrentInteractable = nullptr;
}
6/
The stack trace didn't make sense so I tried re-running the same exact test. This time, I couldn't reproduce it after ten minutes. I was too tired to continue mashing on my keyboard so I did what anyone would do: create another timer to trigger the key automatically. (Thank you EnhancedInput!)
5/
I wondered if the key trigger was the issue so in addition to the timer, I manually pressed the key repeatedly. So now, the Actor might die either as a result of the timer or because of my key presses. After a few minutes, Visual Studio complained about a null pointer access!
4/
(For reference, we're using Unreal 5) So first, I created a timer to repeatedly spawn and destroy the Actor. I also set the console flag "gc.CollectGarbageEveryFrame 1" to force Unreal's garbage collector (GC) to run every frame. I've caught some bad bugs this way. No luck this time, though.
3/
In our playtest build, the game crashed when we pressed a key on the keyboard to trigger an event. What we expected was an Actor object to be destroyed, not a crash. Unfortunately, I couldn't reproduce the crash in my development version nor in the playtest build. Why should life be easy?
2/