ZScript_Basics

🟢 «< BACK TO START

🔵 « Previous: Event Handlers 🔵 » Next: Inventory


Player, PlayerInfo and PlayerPawn

Table of Contents

Overview

The concept of the “player” is represented in ZScript by several different entities, and it’s important to understand what is what when interacting with player data in any way.

Aside from you, the actual physical person playing the game, in the context of ZScript the term “player” primarily refers to one of two things:

  1. PlayerPawn: PlayerPawn is a base ZScript class, a special subclass of Actor that is specifically designed to be controlled by the player. Every game supported by GZDoom defines its own variation: DoomPlayer, HereticPlayer, FighterPlayer, etc. (see the wiki). Player pawns are somewhat similar to monsters, having state sequences like Spawn, See, Missile, Death, yet they have no AI and their actions are controlled by the input from the player. When the player is given an item, or a monster targets them, all of those actions are directed the player-controlled PlayerPawn.

  2. PlayerInfo: PlayerInfo is a special struct attached to every player pawn via a player pointer, which mostly serves as a container for various player-specific data. PlayerInfo contains such data as controls input, field of vision, player health, sprites being drawn on the screen (such as player weapons), currently selected weapon, and so on.

Note: a struct is a data structure similar to a class, generally simpler in nature (for example, it doesn’t support inheritance).

When you want to check for some player-specific data, change player properties or behavior, you may need to interact either with PlayerInfo or PlayerPawn, and figuring out which one you need may be confusing. It’s exacerbated by the fact that PlayerPawn and PlayerInfo share certain fields.

This chapter will cover the basics of this relationship.

PlayerInfo and PlayerPawn: what stores what

First of all, PlayerInfo and PlayerPawn are interconnected. Normally there’s always a PlayerInfo struct attached to a PlayerPawn (exceptions are possible but uncommon), so all player pawns have access to their PlayerInfo structs, and vice versa.

These pointers can be strung one after another. For example:

The nuaces of getting pointers to PlayerInfo or PlayerPawn will be covered later in the article; first, you need to know why the whole thing is important.

As mentioned, when you want to interact with player data, you may need either PlayerPawn or PlayerInfo, depending on the case.

You may notice that PlayerPawn comes with a bunch of fields and properties; however, if you try to access those values dynamically, in some cases you’ll run into issues. For example, PlayerPawn has a viewheight field that determines the height of the player’s eye level above the floor, but PlayerInfo also has the same field. In practice, the value defined in the PlayerPawn is just the default value; once it’s spawned, this value is transferred into the PlayerInfo’s viewheight field which then stores the dynamic value. So, if you have a ppawn pointer to a PlayerPawn, you can read ppawn.viewheight, but modifying it is meaningless since modifying the default values does not affect already existing class instances. Instead you’d need to interact with ppawn.player.viewheight, which contains the current value that actually affects the camera placement.

So, for example, if you want to modify player’s viewheight from an Inventory, you’d do this:

if (owner && owner.player)
{
    owner.player.viewheight = <value>;
}

And you would NOT do this:

// This won't produce any errors but also won't change
// anything visibly:
if (owner)
{
    let ppawn = PlayerPawn(owner);
    if (ppawn)
    {
        ppawn.viewheight = <value>;
    }
}

At the same time, properties like jumpz or viewbob actually are PlayerPawn properties, and changing them requires doing that on player.mo, not on player. Note, if you already have access to the PlayerPawn as Actor, you just need to cast it as PlayerPawn. For exampe, from Inventory:

// This could be done in DoEffect()
if (owner)
{
    let ppawn = PlayerPawn(owner);
    if (ppawn)
    {
        ppawn.jumpz *= 2;
    }
}

So, let’s try to sum it all up and make things a bit clearer:

Types of properties defined in PlayerPawn and PlayerInfo

  1. Actor properties used by PlayerPawn: Some properties of the PlayerPawn class are inherited directly from Actor: raidus, height, painchance, obituary and a bunch of actor flags. They can be read and modified by having an Actor-type pointer to the player pawn and don’t require casting it as PlayerPawn. (So, for example, from an Inventory the owner pointer will work).

  2. Defined in PlayerPawn but used by the PlayerInfo struct: A bunch of properties are defined in the PlayerPawn but only as defaults; those default values are then transferred to the related PlayerInfo struct which stores the values dynamically. The easiest way to check which ones they are is to look at the code for the PlayerInfo struct and the code for PlayerPawn: you’ll notice that both of these have a number of fields with identical names—those fiels are the ones that should be accessed and modified through PlayerInfo, not PlayerPawn (since it only contains the default values).

  3. Defined in PlayerPawn and used by it: There are a few fields unique to the PlayerPawn class that are used by it directly and don’t get transferred to its PlayerInfo struct. Examples of such properties are soundclass, jumpz, attackZOffset and some others. To figure out what they are, just check if that specific property is defined in PlayerPawn but is not defined in PlayerInfo.

  4. Defined in PlayerInfo and used by it: Finally, there are certain fields that are contained solely inside the PlayerInfo struct. A lot of them can be found on the wiki page for PlayerInfo. One common example is the cmd field that stores the buttons currently pressed by the player, and the oldbuttons field that stores the buttons that were pressed during the previous tick.

There are also a few edge cases. For example, health technically falls under the 1st category, yet PlayerInfo has its own health field. Both fiels are updated at the same time, but, for example, HUDs only check for health in PlayerInfo, not PlayerPawn.

The trickiest case is category 2: the values that can be modified in a PlayerPawn but should be actually modified in PlayerInfo if you want to see any effect of that at runtime.

For other cases you’ll simply have to rely on the source code, the wiki and your own memory to remember what goes where.

Data access

Now that you know that you may need to interact both with PlayerInfo and PlayerPawn, you need to know how to access them. We already covered the basics earlier:

But how exactly do you get an initial pointer to one or the other? There are multiple cases for that.

Global PlayerInfo and PlayerPawn access

All PlayerInfo structs are put into a global players array. By using players[<index>] where index is the player number, starting with 0. Note, this array is fixed-size, its size is always equal to the value of a global MAXPLAYERS constant (which is currently 8, since that’s the maximum number of players GZDoom supports). This means that you always have to null-check the entries, since, if there are fewer than 8 players, some entries in that array will be null.

Note, since PlayerInfo is not an actor, null-checking it requires a special function: PlayerInGame[<number>] where number is the number of the player; it returns true if the player is in the game.

As mentioned above, PlayerInfo structs have a mo pointer to their PlayerPawn, so you can use players[<index>].mo to get access to a specific PlayerPawn. Of course, you need to null-check both the PlayerInfo and the PlayerPawn in this case.

Example:

if (PlayerInGame[0] && players[0].mo)
{
    players[0].mo.GiveInventory("BFG9000", 1);
}

This piece of code will give Player #1 a BFG 9000.

Note, however, that in the absolute majority of cases you do not want to use player numbers directly, since if you create gameplay scripts that are tied to the player number, they won’t be compatible with multiplayer (for example, the script above won’t give the other players a BFG).

For cases like this you’d iterate through the players array and apply the necessary effect to all of them:

for (int i = 0; i < MAXPLAYERS; i++)
{
    if (PlayerInGame[i] && players[i].mo)
    {
        players[i].mo.GiveInventory("BFG9000", 1);
    }
}

This will give all players a BFG9000, and it can be called practically from anywhere.

As another example, this will heal all players to 100:

for (int i = 0; i < MAXPLAYERS; i++)
{
    if (PlayerInGame[i] && players[i].mo)
    {
        players[i].mo.GiveBody(100, 100);
    }
}

Notes on the examples:

Generic PlayerPawn and PlayerInfo access

In all cases when an actor has to interact with the player as an actor, it’ll have a pointer to the PlayerPawn. For example, a monster targeting a player will have a target pointer to their PlayerPawn; Inventory classes in the player’s inventory will have an owner pointer to the PlayerPawn that holds them. Do note, however, that all of these are Actor pointers, not PlayerPawn pointers (since monsters can target non-player actors, and items can be placed in non-player inventories).

Of course, you can easily check if any of those pointers is a PlayerPawn, and from there do whatever you need to that PlayerPawn or the related PlayerInfo struct. There are 3 ways to do that. I’ll use an Inventory class and an owner pointer as an example:

  1. Use the is operator to check the pointer inherits from PlayerPawn:
class TestItem : Inventory
{
    override void DoEffect()
    {
        super.DoEffect();
        // Don't forget to null-check the owner:
        if (!owner)
            return;
        if (owner is "PlayerPawn")
        {
            // entered when the owner is a PlayerPawn
        }
    }
}
  1. Cast the pointer as PlayerPawn (see Pointers and casting) and null-check it:
class TestItem : Inventory
{
    override void DoEffect()
    {
        super.DoEffect();
        if (!owner)
            return;
        let ppawn = PlayerPawn(owner);
        if (ppawn)
        {
            // entered when the owner is a PlayerPawn
        }
    }
}
  1. Arguably the simplest way: just check if the pointer has a player field attach to it. It will not abort if the field deosn’t exist, and doing that doesn’t require casting:
class TestItem : Inventory
{
    override void DoEffect()
    {
        super.DoEffect();
        if (!owner)
            return;
        if (owner.player)
        {
            // Entered when the owner has a 'player' field, which essentially
            // guarantees it's a player. Note: you will still need to cast
            // it as PlayerPawn if you want to get access to PlayerPawn-specific
            // properties.
        }
    }
}

Note: if you want to access something specific to the PlayerPawn, you will still need to cast the pointer in question as PlayerPawn. However, if you want to access something specific to PlayerInfo, casting is not required: you simply need to use the pointer’s player field.

For a more specific example, let’s utilize readyweapon—a PlayerInfo field that contains a pointer to the weapon currently selected by the player. Here’s how we can do it from an Inventory:

class WeaponWeightControl : Inventory
{
    override void DoEffect()
    {
        super.DoEffect();
        // Null-check the owner:
        if (!owner)
            return;
        // Check if the owner is a player, has a weapon selected,
        // and that weapon is a Chaingun:
        if (owner.player && owner.player.readyweapon && owner.player.readyweapon.GetClass() == "Chaingun")
        {
            // If so, set their speed to 80% of default:
            owner.speed = owner.default.speed * 0.8;
        }
        else
        {
            // Otherwise reset their speed to default:
            owner.speed = owner.default.speed;
        }
    }
}

In this example the player’s maximum movement speed is reduced when they’re holding a specific weapon.

Notes:

Consoleplayer

Consoleplayer is a global variable that will always return the number of the player who is playing the game. So, while player numbers themselves are global—e.g. player #1 is always whoever started the game, and they are player #1 for all players in the game—consoleplayer will return different numbers for every game client in net play.

Since it’s a number, using players[consoleplayer] you can access the PlayerInfo struct of the owner of the game, and players[consoleplayer].mo gives access to the related PlayerPawn. Do note, that you should be VERY careful with this pointer and preferably avoid changing anything based on consoleplayer. Doom multiplayer is wholly reliant on synchronization: it only works as long as all the gameplay data is synced and identical between all the players. However, if you perform any code that is somehow related to consoleplayer, that code will not be synced across the network, and if that code has any effect on the playsim whatsoever, it’ll immediately break.

Normally consoleplayer is meant to be interacted only within the UI context (meaning, menus and HUDs), since the UI only affects what’s shown on the player’s screen and, more importantly, UI can’t affect the playsim: UI scope can read from it (that’s how, for example, status bar displays how much ammo you have) but not modify it.

There are some very specific cases where consoleplayer can be utilized so that a specific object is displayed for only one player. However, doing that requires a good understanding of where randomization and synchronization occurs, so I will not provide examples at this time.

CPlayer

CPlayer is a HUD-specific pointer to the PlayerInfo of the player who is playing the game. It’s available to all clases that inherit from BaseStatusBar—i.e. HUDs. CPlayer is basically the same as players[consoleplayer], but it only exists for HUDs and can be used absolutely safely, since HUDs are a part of the UI scope and can’t modify anything in the playsim.

This pointer will be covered in more detail in a planned chapter on HUDs.

Voodoo dolls

Voodoo dolls are a specific Doom bug that eventually became a feature and is still supported by most source ports, including GZDoom. This bug/feature occurs when a map author places more than one Player Start object with the same player number, such as two Player 1 starts, in different places in a map. This will create two PlayerPawn actors that are both attached to the same PlayerInfo struct, but the player will only have actual control over one of those actors. The second PlayerPawn is what is described as a “voodoo doll”: dealing damage or healing it (by pushing it into healing items) will transfer the damage/healing to the other PlayerPawn.

This feature is utilized by some mappers to create unexpected “death exits” or timed effects. You can read more about it on the Doom Wiki or watch Decino’s video about it.

Sometimes you may need to check if a specific PlayerPawn is being utilized by a player, or is a voodoo doll. For example, if you have a custom PlayerPawn actor that has this:

override void PostBeginPlay()
{
    super.PostBeginPlay();
    GiveInventory('Clip', 10);
}

as a result, this player will receive 1 Cell not only when its PlayerPawn spawns, but also for every one of its voodool dolls, because voodoo dolls will call their PostBeginPlay() as well, but the items received by voodoo dolls will be transferred to the player.

To avoid this, you need to check that the specific PlayerPawn you’re operating on is not as voodoo doll. This can be done with a simple check:

override void PostBeginPlay()
{
    super.PostBeginPlay();
    if (player && player.mo && player.mo == self)
    {
        GiveInventory('Clip', 10);
    }
}

This check first checks if there’s a PlayerInfo struct attached to the calling actor, then if that PlayerInfo struct has a valid mo pointer, and then it checks that mo pointer is equal to the calling actor. For voodoo dolls this check will return false, but for the original PlayerPawn it’ll return true.


🟢 «< BACK TO START

🔵 « Previous: Event Handlers 🔵 » Next: Inventory