[SOLVED] RandomSpawner adding spawning conditions

Ask about ACS, DECORATE, ZScript, or any other scripting questions here!

Moderator: GZDoom Developers

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!)
User avatar
Virathas
Posts: 249
Joined: Thu Aug 10, 2017 9:38 am

[SOLVED] RandomSpawner adding spawning conditions

Post by Virathas »

Hey everyone!

Since ZScript was implemented a lot options appeared. And i believe it is time to "remaster" a few features to work better than they used to be:

In this case i wanted to improve my RandomSpawners to have additional limits in spawning, specificly spawned actor height and radius. While i try to use "similar" size monsters to reduce potential problems, some need to be higher. For instance, as a DoomImp potential replacement a Heretic Knight can spawn. For many monsters i can create an ACS script that simply checks if the actor is too high, and if so, remove and then place the spawner again.

The problems start with "radius" as it is not as simple to verify a monsters is "fitting". And the problems continue with Boss-like monsters (Mancubus, Arachnotron, Baron of Hell, Cyberdemon, Spidermastermind), as the earlier system will break any Boss event checks.

Frankly, I have no clue how to improve the RandomSpawner, although it's code does look like i could add some more "conditions" to spawn.

To picture an example:

Code: Select all

Actor DoomImpSpawner : RandomSpawner replaces DoomImp
{
	DropItem "DoomImpDropper", 255, 28 
	DropItem "KnightDropper", 255, 10 // this one is taller than original imp
}
Actor Cacospawn : RandomSpawner replaces CacoDemon
{
	Dropitem "CacoDemonDropper", 255, 50
	Dropitem "TerranWraith", 255, 5 // This one is larger
}
In a related matter, could the RandomSpawner at spawn time read what classes of players/game settings are set to further refine the setting? For example disabling particular monsters or restricting item spawns(This one would be trmendous).
Last edited by Virathas on Mon Dec 07, 2020 7:13 am, edited 1 time in total.
User avatar
Virathas
Posts: 249
Joined: Thu Aug 10, 2017 9:38 am

Re: RandomSpawner adding spawning conditions

Post by Virathas »

Ok, i was successfully able to add the monster height restriction, with no ill effects... mostly.

The code of the spawner so far is:

Code: Select all

class SuperSpawner : RandomSpawner
{
        // Test for if item
	static bool IsItem(DropItem di)
	{
		class<Inventory> pclass = di.Name;
		if (null == pclass)
		{
			return false;
		}

		if (GetDefaultByType(pclass).amount > 0)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	override void BeginPlay() // Most is unchanged
	{
		DropItem di;   // di will be our drop item list iterator
		DropItem drop; // while drop stays as the reference point.
		int n = 0;
		bool nomonsters = sv_nomonsters || level.nomonsters;

		Super.BeginPlay();
		drop = di = GetDropItems();
		if (di != null)
		{
			while (di != null)
			{
				if (di.Name != 'None')
				{
					if (!nomonsters || !IsMonster(di)) 
					{
						int amt = di.Amount;
						if (amt < 0) amt = 1; // default value is -1, we need a positive value.
						n += amt; // this is how we can weight the list.
					}
					di = di.Next;
				}
			}
			if (n == 0)
			{ // Nothing left to spawn. They must have all been monsters, and monsters are disabled.
				Destroy();
				return;
			}
			// Then we reset the iterator to the start position...
			di = drop;
			// Take a random number...
			n = random[randomspawn](0, n-1);
			// And iterate in the array up to the random number chosen.
			while (n > -1 && di != null)
			{
				if (di.Name != "None" && IsItem(di)) // Debug
				{
					A_Log(TEXTCOLOR_RED .. di.Name .. "is an item" .. Pos);
					class<Inventory> itemek = di.Name;
				}
				if (di.Name != 'None' &&
					(!nomonsters || !IsMonster(di)))
				{
// The following functions if there is vertical room for the actor
					Class<Actor> cls = di.Name;
					readonly<Actor> newmobj= GetDefaultByType(cls);
					double SectorHeight = ceilingz - floorz;
//					A_Log(TEXTCOLOR_RED .. newmobj.RestrictedToPlayerClass .. Pos);
					if (SectorHeight >= newmobj.Height)
					{
					int amt = di.Amount;
					if (amt < 0) amt = 1;
					n -= amt;
					if ((di.Next != null) && (n > -1))
						di = di.Next;
					else
						n = -1;
					}
					else
					{
						A_Log(TEXTCOLOR_RED .. "Actor re-randomized at" .. Pos); // Debug text
						di = di.Next;
					}
				}
				else
				{
					di = di.Next;
				} // No more edited stuff
			}
			// So now we can spawn the dropped item.
			if (di == null || bouncecount >= MAX_RANDOMSPAWNERS_RECURSION)	// Prevents infinite recursions
			{
				Spawn("Unknown", Pos, NO_REPLACE);		// Show that there's a problem.
				Destroy();
				return;
			}
			else if (random[randomspawn]() <= di.Probability)	// prob 255 = always spawn, prob 0 = almost never spawn.
			{
				// Handle replacement here so as to get the proper speed and flags for missiles
				Class<Actor> cls = di.Name;
				if (cls != null)
				{
				//	A_Log(TEXTCOLOR_RED .. test.Height);
					readonly<Actor> newmobj= GetDefaultByType(cls);
					double SectorHeight = ceilingz - floorz;
					if (SectorHeight < newmobj.Height)
					{
					A_Log(TEXTCOLOR_RED .. "Knight too high at" .. Pos);
					}
					Class<Actor> rep = GetReplacement(cls);
					if (rep != null)
					{
						cls = rep;
					}
					
				}
				if (cls != null)
				{
					Species = Name(cls);
					readonly<Actor> defmobj = GetDefaultByType(cls);
					Speed = defmobj.Speed;
					bMissile |= defmobj.bMissile;
					bSeekerMissile |= defmobj.bSeekerMissile;
					bSpectral |= defmobj.bSpectral;
				}
				else
				{
					A_Log(TEXTCOLOR_RED .. "Unknown item class ".. di.Name .." to drop from a random spawner\n");
					Species = 'None';
				}
			}
		}
	}
}
Now i am trying to create additional restrictions for item spawning depending on present player classes. For example:
2 players in game, Marine and Hexen's Cleric - so only items for them appear and no items for Corvus.

I still am thinking how to create a list of player classes available, so i can iterate

And additionally, i have no idea how to test for "radius" when spawning, especially considering the fact that enemies can be placed pretty tight and still right.
User avatar
Player701
 
 
Posts: 1631
Joined: Wed May 13, 2009 3:15 am
Graphics Processor: nVidia with Vulkan support

Re: RandomSpawner adding spawning conditions

Post by Player701 »

Virathas wrote:And additionally, i have no idea how to test for "radius" when spawning, especially considering the fact that enemies can be placed pretty tight and still right.
You can spawn an actor and call TestMobjLocation to verify that it isn't blocked by anything. According to the source code, it checks for both radius and height blockage, so you probably don't need your own code for the latter. If TestMobjLocation returns false, immediately destroy the spawned actor and try again.

UPD:
Virathas wrote:Now i am trying to create additional restrictions for item spawning depending on present player classes. For example:
2 players in game, Marine and Hexen's Cleric - so only items for them appear and no items for Corvus.
If you're using Inventory.RestrictedTo and Inventory.ForbiddenTo, you can spawn an item and call this method:

Code: Select all

private static bool IsUsefulToAnyPlayer(Inventory item)
{
    for (int i = 0; i < MAXPLAYERS; i++)
    {
        if (!playeringame[i])
        {
            continue;
        }
        
        if (item.CanPickup(players[i].mo))
        {
            return true;
        }
    }
    
    return false;
}
If it returns true, keep the item. Otherwise, destroy the item and try again.
User avatar
Virathas
Posts: 249
Joined: Thu Aug 10, 2017 9:38 am

Re: RandomSpawner adding spawning conditions

Post by Virathas »

The TestMobjLocation works like a charm! While i still need to insert a few failsafes to prevent game crash/lockup it does solve this problem :)

For items(and monsters actually) i never thought of spawning them and running their functions - i tried checking their "default types". Though it did occur to me, that simply using the restricted/forbidden system (That covers majority of cases), will have a few "holes" in the system i.e There are no eligible classes to pick ANY item of the particular spawner. So i did think: Add variables to the actor, a list of player classes akin to the restricted/forbidden ones, that simply say "if only players of this classes are in game first item spawned will be good (since we don't care about an item in this case). As an improvement to that, i was thinking of another variable "FallbackActor" - actor to spawn if "we dont care what to spawn" or some other error occurs.

Thing is, i was unable to add a new variable that i was able to access in DECORATE.
Is it possible to define a new variable/property and then access it via DECORATE? Or do i have to remain fully in ZScript in this case?
User avatar
Player701
 
 
Posts: 1631
Joined: Wed May 13, 2009 3:15 am
Graphics Processor: nVidia with Vulkan support

Re: RandomSpawner adding spawning conditions

Post by Player701 »

Virathas wrote:There are no eligible classes to pick ANY item of the particular spawner.
I get it that such a thing can happen, but I'm not sure if I fully understood the way you were trying to solve it. IMO, this should be done via the spawner itself, not by modifying item classes. Add a property to your random spawner class and use its value to spawn a fallback item if it turns out that no item from the normal list can be picked up by any player. You can define your property as follows:

Code: Select all

class MyRandomSpawner : RandomSpawner
{
    private class<Inventory> _fallbackItemClass;
    
    property FallbackItemClass: _fallbackItemClass;
    
    // TODO: Code that uses the value of _fallbackItemClass
}
Then, when you subclass your spawner to define lists of actors to spawn, specify the fallback item class when necessary:

Code: Select all

class ConcreteSpawner1 : MyRandomSpawner
{
    Default
    {
        // TODO: List of items goes here
        
        MyRandomSpawner.FallbackItemClass 'HealthBonus';
    }
}

class ConcreteSpawner1 : MyRandomSpawner
{
    Default
    {
        // TODO: List of items goes here
        
        MyRandomSpawner.FallbackItemClass 'ArmorBonus';
    }
}
Note that in your code, you should use _fallbackItemClass to get the value. If you don't set it, it will be null by default.

Also see: ZScript custom properties
User avatar
Virathas
Posts: 249
Joined: Thu Aug 10, 2017 9:38 am

Re: RandomSpawner adding spawning conditions

Post by Virathas »

That was exactly what i wanted to do, i might have written my thoughts too confusing,

I tried adding either variable OR property to the spawner, but never both - that's why i failed to add them.

Everything now works like i wanted, i still have some "kinks" i need to work out for this, and i'll happily share the "improved", or at least with additional conditions, in this thread.

Thanks a lot for help!
User avatar
Virathas
Posts: 249
Joined: Thu Aug 10, 2017 9:38 am

Re: RandomSpawner adding spawning conditions

Post by Virathas »

Apparently i did come across a problem with "monster replacement checking: While the script technically works, with Spawn monster -> Check if it fits, get data -> Destroy it, some data still persists - i did see that "monster count goes up when spawned but is not reduced upon destroying. I do not know if it is the only thing "remaining". Since that was not working, i've tried getting the data by getting default data of an actor: readonly<actor> Monster = GetDefaultByType(class), and then accessing Monster.TestMobjLocation(), and afterwards destroying it. to be safe. However, that created a completely different issue - spawning "phantom" monsters, fully invisible, undetectable, untouchable, but active and deadly nonetheless. Is there a better way to do so?
User avatar
Player701
 
 
Posts: 1631
Joined: Wed May 13, 2009 3:15 am
Graphics Processor: nVidia with Vulkan support

Re: RandomSpawner adding spawning conditions

Post by Player701 »

I'm pretty sure what you're attempting is considered undefined behavior. To fix the issue with the kill counter, simply call ClearCounters before destroying the actor.
User avatar
Virathas
Posts: 249
Joined: Thu Aug 10, 2017 9:38 am

Re: RandomSpawner adding spawning conditions

Post by Virathas »

And yet another problem surfaced, one i did not expect. While the spawner and all conditions are working perfectly fine, sometimes some items are not "restricted". After some debugging, I have concluded that simply the spawner is initalized way before Players are actually initalized, so no item restrictions apply at that time. While in most cases it is not a problem, since Player spawns are usually set before other items, i cannot "trust" that it will always be so. I thought of 3 different solutions for the problem, although I am not sure if they are even valid.
1. Instead of a spawner replacer, create a "new" actor, that, after 1 tick, will remove itself and create the spawner.
2. Add some sort of delay or wait function in the spawners "BeginPlay" (I have no idea how to do this, if it is even possible)
3. Move all spawning further down the line, from BeginPlay to PostBeginPlay, hoping that all Players will be initialized by then.
User avatar
Player701
 
 
Posts: 1631
Joined: Wed May 13, 2009 3:15 am
Graphics Processor: nVidia with Vulkan support

Re: RandomSpawner adding spawning conditions

Post by Player701 »

Hmm. You might be right. I've looked up the way the engine handles the class filter for mapthings, and it doesn't use players[...].mo for that. Instead, it uses the data directly from PlayerInfo (aka players[...]). You can get the class via players[...].cls, but since Inventory::CanPickup accepts an Actor and not a class, you would have to replicate its functionality, which wouldn't be entirely robust considering that CanPickup is virtual and can be overridden in derived classes. If you don't want to copy-paste code from gzdoom.pk3, I suggest you go with option #3 and see if it solves your problem.
User avatar
Virathas
Posts: 249
Joined: Thu Aug 10, 2017 9:38 am

Re: RandomSpawner adding spawning conditions

Post by Virathas »

I was unable to get data from players[0].cls, as it it still null at the time.
I did override the postbeginplay, and simply put all the random spawner code, and ran "Super.PostBeginPlay()". So far, i see no problems with it, but we'll see.

Also, great thanks for helping me out here, while i have a ton of experience in Decorate, ACS and significant in C++ and Basic, Zscript still was "mysterious" to me, and you helped me understand much of it :)
User avatar
Virathas
Posts: 249
Joined: Thu Aug 10, 2017 9:38 am

[SOLVED] Re: RandomSpawner adding spawning conditions

Post by Virathas »

So it is done, and works as intended (well, for me, there might be some issues with it, but not affecting anything that it is planned for. I am sharing the code for it here, as I am sure some peopple might find it useful.
Spoiler:

Return to “Scripting”