Levels: A Hub with Spokes

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
Sir Robin
Posts: 392
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Levels: A Hub with Spokes

Post by Sir Robin »

I'm creating a set of level that instead of playing through them sequentially, there will be a master map with links to each level. When finishing a level the player goes back to the master map. I'm thinking like Mario64 or any other hub-and-spokes type of game.

I think I how how to start this. Create a cluster, master map is level 1, the rest are 2..n, for each one the nextmap is 1 so it goes back to the master when completed.
Other features I want:
1) When a player dies on a map, it doesn't auto load last save or respawn at map start, but goes back to the master map. Not immediately when they die but when they press use to load/spawn.
2) Track and keep some info from each map, for example number of attempts, number of successes, number of deaths, also high scores for kills, items, secrets, par, etc. I could do this by storing all that on a player inventory item, but I think that's kind of a bad idea because if another mod messes with the player inventory I could lose it. So I'm thinking that the thing to do is have a static event handler and a regular event handler. The static event handler persists between levels but doesn't exist in save games, and the normal event handler exists in save games but doesn't persist between levels. So I'm thinking I create one of each and an object to hold the data and they pass it between them at the start and end of each level. Is that a good way to do that or is there something else?
User avatar
Sir Robin
Posts: 392
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Levels: A Hub with Spokes

Post by Sir Robin »

The hand-off thing wasn't difficult to do:

Code: Select all

class LevelStatsManager
{
   //level stats variables go here
}

class LevelStatsSuperHandler : StaticEventHandler
{
    //don't modify this in a level, this is only used for passing stats between levels
    LevelStatsManager LevelStats;
    
    override void OnRegister()
    {
        LevelStats = new("LevelStatsManager");
    }
}

class LevelStatsHandler : EventHandler
{
    //modify stats here
    LevelStatsManager LevelStats;

    override void OnRegister()
    {
        if (!LevelStats)
        {
            console.printf("Get LevelStats from master");//DEBUG
            LevelStats = LevelStatsSuperHandler.GetInstance().LevelStats;
        }
        else if (LevelStats != LevelStatsSuperHandler.GetInstance().LevelStats)
        {
            console.printf("Push LevelStats to master");//DEBUG
            LevelStatsSuperHandler.GetInstance().LevelStats = LevelStats;
        }
    }
}
 
That works for a game played in a single session. But saving and loading the game will break it - you loose any stats you made since the last save. That means I can't keep track of deaths this way because that stat will never be saved.
I need somewhere to store and save this data that is outside of the game save. Would this be a good place to use CVARs or is that a bad idea for any reason?
User avatar
Virathas
Posts: 220
Joined: Thu Aug 10, 2017 9:38 am

Re: Levels: A Hub with Spokes

Post by Virathas »

I was wondering, couldn't you just use ACS for that? While i definitely prefer ZScript, this is something that ACS could actually be more useful for.
Simple sample copy pasted script:

Code: Select all

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

global int 1:PlayerDeath[];

script "PlayerDeath" death
{
	PlayerDeath[PlayerNumber()] = PlayerDeath[PlayerNumber()] + 1;
}
Alternative idea, is to use an enter handler to spawn a dummy actor in the map, that will hold data, and simply, once the player is moved to other map, the data is temporarily moved to the player (in the form of inventory), and once in the hub, placed in some other actor/variable/whatsoever

For the first point though, i think some overrides to playerpawn will suffice, though I have yet to edit that part of the code.
User avatar
Sir Robin
Posts: 392
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Levels: A Hub with Spokes

Post by Sir Robin »

Thanks for the input. I know hardly anything about ACS, not even sure how to hand off variables from Zscript to ACS and back again. And don't I need a separate compiler for ACS?

What you're saying about using a dummy actor and an inventory item is essentially what I did in the code above. I created a class called LevelStatsManager to store the data, then an event handler and a static event handler. Their job was just to pass that data class from level to level and in and out of save game. But then I realized that I actually don't want that data in a save game, I want death counts to persist outside somehow. I've found a way to use CVARs for what I want to do - I created CVARs in the NoSave scope without the NoArchive flag, so they are saved in the users local config and not in the same games. This means they persist even if you load a game, and even if you start a new game or quit and reload GZDoom, your high scores and death counts and everything else are all still saved.

I'm working on the mod now, I'll post a link when it working well enough to demonstrate.

Something I still haven't figured out - how disable auto load last save game after death and how to send the player to a specific map after death.
User avatar
Virathas
Posts: 220
Joined: Thu Aug 10, 2017 9:38 am

Re: Levels: A Hub with Spokes

Post by Virathas »

For the "respawn block" you can override DeathThink of the PlayerPawn. An example, that prevents respawn, and will spam the console with info that use button is pressed.

Code: Select all

class DoomRespawnForbidden : DoomPlayer
{
	override void DeathThink ()
	{
		let player = self.player;
		int dir;
		double delta;

		player.Uncrouch();
		TickPSprites();

		player.onground = (pos.Z <= floorz);
		if (self is "PlayerChunk")
		{ // Flying bloody skull or flying ice chunk
			player.viewheight = 6;
			player.deltaviewheight = 0;
			if (player.onground)
			{
				if (Pitch > -19.)
				{
					double lookDelta = (-19. - Pitch) / 8;
					Pitch += lookDelta;
				}
			}
		}
		else if (!bIceCorpse)
		{ // Fall to ground (if not frozen)
			player.deltaviewheight = 0;
			if (player.viewheight > 6)
			{
				player.viewheight -= 1;
			}
			if (player.viewheight < 6)
			{
				player.viewheight = 6;
			}
			if (Pitch < 0)
			{
				Pitch += 3;
			}
			else if (Pitch > 0)
			{
				Pitch -= 3;
			}
			if (abs(Pitch) < 3)
			{
				Pitch = 0.;
			}
		}
		player.mo.CalcHeight ();
			
		if (player.attacker && player.attacker != self)
		{ // Watch killer
			double diff = deltaangle(angle, AngleTo(player.attacker));
			double delta = abs(diff);
	
			if (delta < 10)
			{ // Looking at killer, so fade damage and poison counters
				if (player.damagecount)
				{
					player.damagecount--;
				}
				if (player.poisoncount)
				{
					player.poisoncount--;
				}
			}
			delta /= 8;
			Angle += clamp(diff, -5., 5.);
		}
		else
		{
			if (player.damagecount)
			{
				player.damagecount--;
			}
			if (player.poisoncount)
			{
				player.poisoncount--;
			}
		}		

		if ((player.cmd.buttons & BT_USE))
		{
			console.printf("%s", "Use Button Pressed");
		}
	}
}
You could have something like an action special in that place to force a map change. You could even have a resurrect function first, and then move the player to the hub level.
User avatar
Sir Robin
Posts: 392
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Levels: A Hub with Spokes

Post by Sir Robin »

ah, interesting, I hadn't thought of it that way. So I could write my own death handler there. Or if I left that code alone, I notice that what it essentially does is set the player state to either Reborn or Enter after they hit use. So maybe I could catch it in the EventHandler PlayerEntered call, but I don't know if another handler would have already done the save game load by then or not. I'll have to do some testing.

Thanks for posting!

Return to “Scripting”