Random Start [Updated 11/28/21]

Post your example zscripts/ACS scripts/etc here.
Forum rules
The Projects forums are only for projects. If you are asking questions about a project, either find that project's thread, or start a thread in the General section instead.

Got a cool project idea but nothing else? Put it in the project ideas thread instead!

Projects for any Doom-based engine (especially 3DGE) are perfectly acceptable here too.

Please read the full rules for more details.
Hey Doomer
Posts: 283
Joined: Sat Sep 25, 2021 3:38 am

Random Start [Updated 11/28/21]

Post by Hey Doomer »

The RandomPlayerStarts keyword will randomize existing spawn points (I have not tried this), but is it possible to have a truly random starting point in a particular level? I did this using ACS with arbitrary limitations:
  • To avoid too many instructions that terminate the script (a runaway), a random search is conducted in a decreasing radius to 64 units
  • The PlayerCheck dummy actor must be within line of sight using CheckSight
  • The new location has a sector height > 64
  • The floor texture isn't blank and the terrain is SOLID (probably not necessary)
DECORATE:

Code: Select all

Actor PlayerCheck
{
	+Invisible
}
ACS:

Code: Select all

#library "randomstart"
#include "zcommon.acs"

script "randomstart" ENTER
{
    int tid = 21000;
    int pid = tid;
    Thing_ChangeTID(0, pid);

    int x = GetActorX(0) >> 16;
    int y = GetActorY(0) >> 16;

    int rad = GetUserCVar(0, "rand_radius");
    bool debug = GetUserCVar(0, "rand_debug");

    if (debug)
    {
        Log(f:GetActorFloorZ(pid), s:", ", f:GetActorCeilingZ(pid), s:", ", s:GetActorFloorTexture(pid));
    }
    while (1==1)
    {
        if (debug)
        {
            Log(s:"Checking radius: ", d:rad);
        }
        int new_x = x + Random(0, rad) - Random(0, rad);
        int new_y = y + Random(0, rad) - Random(0, rad);
        int ceilingZ = GetSectorCeilingZ(0, new_x, new_y) >> 16;
        int floorZ = GetSectorFloorZ(0, new_x, new_y) >> 16;
        int sectorHeight = ceilingZ - floorZ;

        if (sectorHeight > 64)
        {
            tid++;
            Spawn("PlayerCheck", new_x << 16, new_y << 16, floorZ << 16, tid);

            if (debug)
            {
                Log(f:GetActorFloorZ(tid), s:", ", f:GetActorCeilingZ(tid), s:", ", s:GetActorFloorTexture(tid));
            }

            if (CheckSight(tid, pid, 0)==true && GetActorFloorTexture(tid)!="" && GetActorFloorTerrain(tid)=="SOLID")
            {
                if (debug)
                {
                    Log(s:"x: ", d:new_x, s:" y: ", d:new_y, s:" sector height: ", d:sectorHeight, s:" floor: ", s:GetActorFloorTexture(tid));
                    Log(s:"moving...");
                }
                SetActorPosition(pid, new_x << 16, new_y << 16, floorZ << 16, false);
                break;
            }
            rad -= 2;
            if (rad < 64)
            {
                break;
            }
        }
    }
}
I've included cvars rand_radius and rand_debug. If the starting area is very small it the script terminates. Otherwise the player is moved to a random point within line of sight before the level appears. I've tested this with a number of random maps, and I haven't seen instances of moving the player into another actor. I suppose that's possible. I also have kept the angle the same, although that can be randomized as well. Generally levels are designed so the player enters a level facing something, and it seems better to keep that as is.

Anyway still playing with this. I'm not sure, for example, if it would crash with multiple hops around the room to (perhaps) start down a corridor. If the script runs too long the player may appear at the mapped spawn point, breaking the effect.

Update
I've added rand_hops as a loop variable to allow the random start to hop around the spawn area. Seems to work.
Last edited by Hey Doomer on Sun Nov 28, 2021 1:42 pm, edited 2 times in total.
Hey Doomer
Posts: 283
Joined: Sat Sep 25, 2021 3:38 am

Re: Random Start [Updated 11/26/21]

Post by Hey Doomer »

Interesting problem: How do I know an actor is on the map?

Initially, I wondered if the ceiling, floor, terrain, and other qualities would make a difference. Then I wondered about CheckSight. These do not appear to be reliable. I am trying tagging each sector and using ThingCountSector. This should work if the counted tid is unique and all sectors are tagged the same. Initial test plays look promising.

Zscript:

Code: Select all

class rs_EventHandler : EventHandler
{
	bool isRestart;
	override void WorldLoaded(WorldEvent e)
	{
		isRestart = e.IsSaveGame;
	}
	override void PlayerSpawned(PlayerEvent e)
	{
		PlayerInfo player = players[e.PlayerNumber];
		if (isRestart)
		{
			player.mo.ACS_NamedExecute("randomstart", 0, 0, 0, 0);
		}
	}
}

class rand_PostProcessor : LevelPostProcessor
{
	// add sector tags to determine if something is on the map
	protected void Apply(Name checksum, String mapname)
	{
		int tag = Cvar.FindCVar("rand_tag").GetInt();
		for (int i = 0; i < Level.Sectors.Size(); i++)
		{
			AddSectorTag(i, tag);
		}
	}
}
I've added a check for IsSaveGame, so the level may continue to be played at a somewhat randomized position from the last save. That's the idea. Not convinced yet that this works.

ACS:

Code: Select all

script "randomstart" (void)
{
    int tid = 21000;
    int pid = tid;
    Thing_ChangeTID(0, pid);

    int rad = GetUserCVar(0, "rand_radius");
    bool debug = GetUserCVar(0, "rand_debug");
    int hops = GetUserCVar(0, "rand_hops");
    int rand_tag = GetUserCVar(0, "rand_tag");

    while (hops > 0)
    {
        int x = GetActorX(pid) >> 16;
        int y = GetActorY(pid) >> 16;

        if (debug)
        {
            Log(s:"Hop: ", d:hops, s:", ", f:GetActorFloorZ(pid), s:", ", f:GetActorCeilingZ(pid), s:", ", s:GetActorFloorTexture(pid));
        }
        while (1==1)
        {
            if (debug)
            {
                Log(s:"Checking radius: ", d:rad);
            }
            // scouting new location
            int new_x = x + Random(0, rad) - Random(0, rad);
            int new_y = y + Random(0, rad) - Random(0, rad);
            int ceilingZ = GetSectorCeilingZ(0, new_x, new_y) >> 16;
            int floorZ = GetSectorFloorZ(0, new_x, new_y) >> 16;
            int sectorHeight = ceilingZ - floorZ;

            if (sectorHeight > 64)
            {
                tid++;
                Spawn("PlayerCheck", new_x << 16, new_y << 16, floorZ << 16, tid);
                if (CheckSight(pid, tid, 0))
                {
                    int n = ThingCountSector(T_NONE, tid, rand_tag);
                    Thing_Remove(tid);
                    if (n > 0)
                    {
                        if (debug)
                        {
                            Log(s:"x: ", d:new_x, s:" y: ", d:new_y, s:" sector height: ", d:sectorHeight, s:" floor: ", s:GetActorFloorTexture(tid));
                            Log(s:"moving...");
                        }
                        SetActorPosition(pid, new_x << 16, new_y << 16, floorZ << 16, false);
                        break;
                    }
                }
                rad -= 2;
                if (rad < 64)
                {
                    break;
                }
            }
        }
        hops--;
    }
}
This is simpler, mostly because I'm no longer looking at textures, which appear to be irrelevant. Before adding the thing count, I noticed that in a number of cases CheckSight fails or, more precisely, one can still be "seen" if off map. I'm still relying on that and searching for the PlayerCheck actor in all sectors.

Dunno... is there an easier way?

Update
Spoiler:

Return to “Script Library”