Dynamic Weapon Spawns (Advanced)

From Valve Developer Community
Revision as of 18:12, 19 February 2006 by Gia (talk | contribs) (completing two subtitles)
Jump to navigation Jump to search

Template:WIP Template:WIP Template:WIP Template:WIP Check discussion first please --gia 14:08, 19 Feb 2006 (PST)


Introduction

Here we will expand on Draco's tutorial Dynamic_Weapon_Spawns. The mechanic used was to have a model entity that spawned a set of weapons depending on the values of six ConVars, all of this at the beginning of the round.

Each ConVar would represent an slot of a Weapon Set. Weapon Sets may not just spawn weapons but other items like ammo boxes, armor or health.

Objectives

This tutorial works for HL2MP, you will have to modify some steps to get it to work on another game. We will try to accomplish the following:

  • Use a resource file with the contents of Weapon Sets: weaponsets.txt
  • Ability to modify, switch, load or save Weapon Sets at any time during the round using ConCommands with their respective AutoComplete functions.
  • Use a Point_Entity CWeaponSetEnt to spawn other entities. Weapons or other Items like armor or health.
  • Inputs/Ouputs as in any Weapon and working. ie. must have OnPlayerPickUp and it must work when the player picks up the spawned weapon.
  • Ability to set the ammo for the items to spawn (amount of health or armor to recover in the other cases).
  • Ability to force the spawning of a specific item regardless what the weapon set says.
  • Inclusion of 4 special spawn rules:
    • <none> which will spawn nothing
    • <random>
    • <allweaps-old> will divide the items available in tiers, like High, Medium and Lower tier Weapons. And only spawn certain tier depending of the slot number.
    • <allweaps-new> will use the same tier division but will spawn more than one tier per slot, each tier has a different chance of being spawned at each slot.
  • If the Weapon Set doesn't have enough slots defined to cover the needs of a map then the values will wrap around the Weapon Set. (ie. A set with only 2 slots and a map entity that requires slot 7, after wrapping 7 around 2 it would end up using slot 1).


The Resource File weaponsets.txt

The file could look like this.

"WeaponSets"
{
	"WeaponSet"
	{
		"Name"			"Pistols"
		"Slots"
		{
			"1"		"357"
			"2"		"ammo_pistol_large"
			"3"		"pistol"
			"4"		"pistol"
			"5"		"ammo_pistol"
		}
	}
	"WeaponSet"
	{
		"Slots"
		{
			"1"		"357"			//only 1 slot
		}
		"Name"			"357s"			//order doesnt matter, Name is last
	}
	"WeaponSet"
	{
		"Name"			"Automatics"
		"Slots"
		{
			"1"		"ar2"
			"2"		"ammo_ar2_alt"
			"5"		"ammo_ar2"		//there's no 4
			"3"		"smg1"			//3 is after 5
			"6"		"ammo_smg1"
			"7"		"<none>"		//same effect as not listing it
			"8"		"ammo_smg1_grenade"
		}
	}
}

The Entity FGD Definition

Let's define the FGD entry to use as a base for the coding.

@include "hl2mp.fgd"

@PointClass base(Weapon) studio("models/items/357ammo.mdl")
		= item_weaponset : "A weapon spawn point that uses weapon sets"
[
	spawnitem(choices) : "Spawn what item?" : -20 : "What item should spawn?" =
	[
		-20:	"Use Weapon Set"	//only this option uses weaponsets.txt
		-3:	"All Weapons Classic"	//this and the following are forced values
		-2:	"All Weapons Modern"
		-1:	"Spawn Random Items"
		0:	"Spawn Nothing"
		1:	"weapon_ar2"
		2:	"weapon_pistol"
		3:	"weapon_smg1"
		4:	"weapon_357"
		5:	"weapon_xbow"
		6:	"weapon_shotgun"
		7:	"weapon_ml"
		8:	"weapon_stunstick"
		9:	"item_grenade (1)"
		10:	"weapon_slam (1)"
		11:	"item_ammo_ar2 (20)"
		12:	"item_ammo_ar2_secondary (1)"
		13:	"item_ammo_pistol (20)"
		14:	"item_ammo_smg1 (45)"
		15:	"item_ammo_357 (6)"
		16:	"item_xbow_bolt (1)"
		17:	"item_box_buckshot (20)"
		18:	"item_ml_grenade (1)"
		19:	"item_smg1_grenade (1)"
		20:	"item_ammo_pistol_large (100)"
		21:	"item_ammo_smg1_large (225)"
		22:	"item_ammo_ar2_large (100)"
		23:	"item_ammo_357_large (20)"
		24:	"item_health (1)"
		25:	"item_battery (100)"
	]

	weaponsetslot(integer)	: "For what weapon slot? [1,n]" : 1
			: "If using Weapon Set what slot to use? [1,n]"

	ammotogive(choices) : "How will ammo to give be calculated?" :	0
			: "Consider health, armor or whatever the item gives as ammo." =
	[
		0:	"0-Use Default for item"
		1:	"1-Use AmmoValues as Absolute Value"
		2:	"2-Use AmmoValues as %Max-Ammo allowed by Recipient"
		3:	"3-Take values from Custom Ammo String"
	]

	ammovalue(string) : "Amount of Ammo" : ""
			: "Read as float, only affects the first type of ammo given by item."
			"The only one usually."

	customammostring(string) : "Custom Ammo String" : ""
			: "For items giving two types of ammo, weapons, with different values." +
			" <GiveIndex>,<value>;<GiveIndex>,<value>... ie. Primary set as Default" +
			" and Secondary set as Relative 0.5: '0,0;2,0.5'"

	//spawnflags(Flags) = [ ] the base class weapon has a flag Start constrained
]

base(Weapon)

By using Weapon as the base then this entity inherits the inputs and ouputs and Name and Orientation parameters. It also gets the Start Constrained flag.


ammotogive(choices) and ammovalue(string)

These are so level designers can use this entity without having to deal with the Custom Ammo String, however they are not neccessary as the custom ammo string can do the same and more.

  • 0: Default - If Box of Rounds always gives 10 ammo then do the same... This option doesnt care about AmmoValues
  • 1: Absolute - If AmmoValues says to give 1 ammo give 1 ammo despite the default 10
  • 2: %Max-Ammo - If the pistol that will receive the ammo takes up to 500 rounds and 50 in mag and Ammovalues says 0.5 then give 500+50 = 550 * 0.5 = 275 rounds. In the case of health we can say the recipient is the player, so he would get 50% health recovered. Same for Armor, and other items.


customammostring(string)

It could work for items that give 'n' types of ammo, provided you modify the code to accept that. When using index 0 for Default the ammo value used doesn't really matter, it could be blank, but it would be better to use another 0 there.


The Code

The whole thing is here?. Now onto the main sections of it.

Main structures

class CWeaponSetEnt;	//Map Entity, cant belong to a namespace because of macros
			//defining their own inside
class CItem_ModAmmoCrate;	//Another entity, this is an ammo box that holds any type
				//of ammo, I use it to make my life simple.
				//You can use hl2mp's ammo box entities, after adding the
				//ability to modify the ammount of ammo they give.

namespace WeaponSetH { //Namespace, just to keep things in order
	struct ItemDef; //Holds constants to make things easier to read
	struct Item;	//Holds data about items
	class CWeaponSet; //Static class that does all the coordination

	struct AllWeapChance;	//Holds data of slots and tiers associated to them
				//to be used by <allweaps-old> and <allweaps-new>
};

Obviously they are using a namespace, so just pretend I used namespace WeaponSetH in the following code blocks.

  • CWeaponSetEnt will use the entity named "item_weaponset"
  • CItem_ModAmmoCrate will use the entity named "item_modammocrate"

[Todo] change the "ModAmmoCrate" to some better name... Mod-ified is fine for the prototype but not for final code


ItemDef

ItemDef is just a bunch of integer constants in a struct instead of a enumeration. Why a struct? because the constants do not belong to a single, continous, enumeration. Also, it allows the use of ItemDef:: to quickly access the constants.

The SPAWN constants are used as itemIndexes (IDs). Despite the un-optimization we could use string indexes over integers (itemNicks), but we require the integers to obtain the Ammo Index (some hl2mp var, see hl2mp_gamerules.cpp::GetAmmoDef, more on this later) and also a way to map the indexes the Hammer entity will use to Item instances (see FGD::spawnitem).

The TIER constants define Item Tiers, the way they are defined it only makes possible to link an item to only one tier unless we used arrays. To allow items to belong to multiple tiers without complicating things, you could make them bit flags and did the proper modifications to the couple of functions that uses this.

Three more constants, they are utility constants, see the code-comments to understand a bit about them.


Item

Ok we know that items (name Weapons, Ammo Boxes, Batteries, anything!) won't change in the middle of the game. So we just need to load the data once (initialize) and read from there without modifications, so it only needs getter functions.

//Contains an item definition
struct Item {
	int itemIndex;	//This Item Index is according to WeaponSetH::ItemDef SPAWN constants
	int ammoCount;	//Base ammo count, used for Ammo Boxes
	char* entityName;	//The entity name associated to this item
	char* itemNick;	//A nick, ent names are long, this way names are easier to users
	int itemTier;	//specific for All Weapons, but may be used for other things
};
//const int itemListSize;	defined in next block
//WeaponSetH::Item itemList[];	defined in next block

WeaponSetH::Item* GetItemByItemIndex(int index);	//ItemIndex is a member of the struct
WeaponSetH::Item* GetItemByNick(const char* itemNick);	//ItemNick is another member
WeaponSetH::Item* GetItemByArrayIndex(int index);	//The array index comes from
							//itemList[] above


If you check the code (weaponset.h), itemList[] and itemListSize are the way they are because it's easier to do maintenance to that initialization matrix. A struct that only defined members can be initialized that way, they are called Aggregates (Initializing Aggregates).

In this block you can see if you added a new weapon and needed to include it here you would just add a line and compile. The itemList would initialize and itemListSize recalculate. You might have to add a SPAWN and/or TIER constant to WeaponSetH::ItemDef though.

// {itemType, ammoCount, entityName, nickName, itemTier },
WeaponSetH::Item itemList[] = {
   {ItemDef::SPAWN_ALLWEAPONS_CLASSIC,	0,
		"",			"<allweaps-old>",	ItemDef::TIER_NOSPAWNABLE	},

   {ItemDef::SPAWN_NOTHING,		0,
		"",			"<none>",		ItemDef::TIER_NOTHING		},

   {ItemDef::SPAWN_PISTOL,		0,
		"weapon_pistol",	"pistol",		ItemDef::TIER_LOWWEAPON		},

   {ItemDef::SPAWN_HEALTH,		25,
		"item_health",		"health",		ItemDef::TIER_OTHERS		},

   {ItemDef::SPAWN_ARMOR,		100,
		"item_battery",		"armor",		ItemDef::TIER_OTHERS_ARMOR	},
};
const int itemListSize = sizeof(WeaponSetH::itemList) / sizeof(WeaponSetH::Item);

As for the getters they return a pointer to a member of the itemList array. Now, in case of failure they would return NULL, but I set them to return the Item NOTHING (which would spawn nothing). Wether this is right or not to you it is just a matter or adding NULL checks where appropiate.

AllWeapChance

CItem_ModAmmoCrate

CWeaponSet

EntList

CWeaponSetEnt

ConCommands

SDK Modifications

How to Improve