RandomSpawner: BeginPlay, PostBeginPlay and loading

Ask about ACS, DECORATE, ZScript, or any other scripting questions here!
Forum rules
Before asking on how to use a ZDoom feature, read the ZDoom wiki first. If you still don't understand how to use a feature, then ask here.

Please bear in mind that the people helping you do not automatically know how much you know. You may be asked to upload your project file to look at. Don't be afraid to ask questions about what things mean, but also please be patient with the people trying to help you. (And helpers, please be patient with the person you're trying to help!)
Post Reply
alexsa2015sa
Posts: 54
Joined: Fri Apr 24, 2020 2:12 pm
Location: Possibly behind *that* curtain

RandomSpawner: BeginPlay, PostBeginPlay and loading

Post by alexsa2015sa »

I'm not sure this qualifies as bug (unnecessary code calls sort of... are?) or just a engine quirk, but it's been bugging me all week and I do not know anymore if this is just me doing something wrong.

From my testing, unwanted (to me, at minimum) behaviour occurs on level load, whether it's hub reload or a map load, with mapthing actors which were replaced with RandomSpawner. Some details differ on map load and level change, but broad strokes are the same.

BeginPlay() is apparently called during (p_mobj.cpp) ConstructActor and FLevelLocals::SpawnMapThing functions, and virtual functions processed then and there, resulting in redundant ChooseSpawn() calls; I can't tell what actually executes, but some calls such as Console.Printf definitely execute. That's bad enough already I guess, but it will produce unwanted behaviours if you attempt to override ChooseSpawn using function calls that don't just read data but write it into a static thinker or something similar.

The behaviour is easily demonstrated by using this code (replacing some common actor) and repeatedly travelling between hub levels or just quickloading:

Code: Select all

class DestroyRandomSpawner : RandomSpawner replaces Ettin
{
	// Override this to decide what to spawn in some other way.
	// Return the class name, or 'None' to spawn nothing, or 'Unknown' to spawn an error marker.
		override Name ChooseSpawn()
	{
		Console.Printf("%s is still thinking!", GetClassName());
		return Super.ChooseSpawn();
	}
	//  For reference, start of a BeginPlay()
	//  override void BeginPlay()
	//  {
	//    // Bool check always passes even if it is the first line, suggesting the value isn't loaded in a savegame yet
	//    if (boolspawn) return; else boolspawn = true;
	//	  Actor.BeginPlay(); // Super does exactly the thing it shouldn't
	//	  let s = ChooseSpawn();
	//
}
On map reload console will be flooded with messages demonstrating that no, these destroyed randomspawners still call super.BeginPlay() (reconstructing mapthings on reload?) and only vanish by PostBeginPlay(). Summoned actors do not exhibit this behaviour like mapthing ones.

Obvious Zscript-side solution is a single bool check before starting on the rest of code... which won't work, likely because of engine-side loading sequence. Only moving entire BeginPlay() override content to PostBeginPlay() works, which is, uh,.. sort of extreme solution.

Can others share thoughts on it? I fixed what I wanted working, but another opinion never hurts.
User avatar
Player701
 
 
Posts: 1710
Joined: Wed May 13, 2009 3:15 am
Graphics Processor: nVidia with Vulkan support
Contact:

Re: RandomSpawner: BeginPlay, PostBeginPlay and loading

Post by Player701 »

You are right, when the map gets initialized for whatever reason, be it a new game, a hub or level transition, or loading a save file, the mapthings will be initialized and spawned as well, therefore BeginPlay will get called for all of them. However, if an existing saved state is present, these map-spawned actors will be immediatelly destroyed and replaced with whatever was in the saved state. (Note that BeginPlay will not be called for saved actors.) Therefore, if your BeginPlay call reads or writes some data from an external source, you have to make precautions necessary to ensure that if an existing saved state is being deserialized, it either doesn't read or write anything at all, or the data gets discarded as soon as the deserialization is done. (Also see here for a similar issue.)

There are several ways around this, the best one being not to use BeginPlay for this stuff in the first place, but in case of RandomSpawner that might be a little difficult. What you can do instead is to use a non-static data source that only exists once the level has been fully loaded. An event handler should be mostly suited for this kind of task, since it will not have been initialized yet when a "temporary" actor attempts to access it. In this case, a simple check can be added to make sure the event handler exists so as not to trigger a VM abort. The event handler itself can communicate data to your static thinker either immediately or at the end of the level when it gets unloaded (depending on your exact use case).

Here's a little ZScript to demonstrate this technique (Don't forget to add the event handler via MAPINFO):

Code: Select all

version "4.5"

class TestZombie : ZombieMan replaces ZombieMan
{
    override void BeginPlay()
    {
        let eh = EventHandler.Find('MyEventHandler');
        Console.Printf("%s", eh == null ? "NULL" : "NOT NULL");

        Super.BeginPlay();
    }

    override void OnDestroy()
    {
        Console.Printf("Destroyed");
        Super.OnDestroy();
    }
}

class MyEventHandler : EventHandler 
{
    override void OnRegister()
    {
        Console.Printf("Registering event handler");
    }
}
When you start a new game (make sure Zombiemen are present on the map), you will see the following console output:

Code: Select all

Registering event handler
NOT NULL
NOT NULL
...
NOT NULL
You can see that the event handler gets registered, and the actors in the map can access it. Now make a save file and load it, and you will see something like:

Code: Select all

Destroyed
Destroyed
...
Destroyed
NULL
NULL
...
NULL
Destroyed
Destroyed
...
Destroyed
Registering event handler
The first bunch of "Destroyed" messages are from the actors from the previous map being destroyed (You will not see them if you load the game from the title screen). Then, the map is initialized, and temporary actors are spawned, but the event handler is not yet available at this point (since the engine knows it's not a new game, and it has to deserialize the handler from the archive instead), so they print "NULL". Then the temporary actors get destroyed, and the event handler (deserialized from the archive along with everything else contained in the saved state) is registered.
Post Reply

Return to “Scripting”