🔵 << Previous: Event Handlers 🔵 >> Next: Inventory
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:
-
PlayerPawn:
PlayerPawn
is a base ZScript class, a special subclass ofActor
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. -
PlayerInfo:
PlayerInfo
is a special struct attached to every player pawn via aplayer
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.
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.
-
Every PlayerPawn instance has a
player
pointer to the PlayerInfo struct that controls it. -
Conversely, every PlayerInfo struct has a
mo
pointer to its PlayerPawn ("mo" stands for "map object").
These pointers can be strung one after another. For example:
-
If a monster is targeting a PlayerPawn, it'll have a
target
pointer to it. (The type of that pointer will beActor
, since any actor can become a target.)-
From that
target
pointer you can usetarget.player
to get access to the related PlayerInfo struct.-
From there you can use
target.player.mo
to access the PlayerPawn again (except the type of that pointer will bePlayerPawn
).- While entirely useless, you can keep going and use
target.player.mo.player
,target.player.mo.player.mo
...
- While entirely useless, you can keep going and use
-
-
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:
-
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 anActor
-type pointer to the player pawn and don't require casting it as PlayerPawn. (So, for example, from an Inventory theowner
pointer will work). -
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).
-
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. -
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 theoldbuttons
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.
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:
-
PlayerPawn has a
player
pointer to the PlayerInfo struct that controls it -
PlayerInfo has a
mo
pointer to its PlayerPawn
But how exactly do you get an initial pointer to one or the other? There are multiple cases for that.
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:
- You can find out more about
for
loops in Appendix 1: Flow Control. - As always with iterating through arrays, you need
i < MAXPLAYERS
, noti <= MAXPLAYERS
, becauseMAXPLAYERS
is the size of the array, which is always bigger than the index of its last element (since indexes begin with 0). GiveBody()
(see on the wiki) is a ZScript-specific healing function.
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:
- Use the
is
operator to check the pointer inherits fromPlayerPawn
:
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
}
}
}
- 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
}
}
}
- 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:
-
readyweapon
is a PlayerInfo-specific field, so we can only access it through the PlayerPawn'splayer
pointer; as a result the item needs to check forowner.player.readyweapon
. -
readyweapon
is not a class name/type; it's a pointer to the instance of a Weapon class, i.e. a specific weapon the player is currently using. See Pointers and casing if you need a refresher on the difference. -
In contrast,
speed
is an Actor property, so we don't need any casting or extra pointers to read and modify it, we can just useowner.speed
directly.
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
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 is covered in more detail in the HUD and statusbar chapter.
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.