Difference between revisions of "Modding:Tutorial/Game Objects"
From DoomRL Wiki
Game Hunter (Talk | contribs) m (minor details) |
Game Hunter (Talk | contribs) m (→API: small clarification error) |
||
(One intermediate revision by one user not shown) | |||
Line 129: | Line 129: | ||
name = "nasty", | name = "nasty", | ||
.... --other prototype keys are added | .... --other prototype keys are added | ||
− | + | OnAction = function(being) --engine hook is called | |
.... --code to be executed in the hook goes here | .... --code to be executed in the hook goes here | ||
end, --function ends | end, --function ends | ||
Line 161: | Line 161: | ||
<source lang="lua"> | <source lang="lua"> | ||
− | being.new(id) --this will | + | being.new(id) --this will allocate a new instance of the being object id to memory |
− | being.destroy(some_being) --this will | + | being.destroy(some_being) --this will deallocate a being instance called some_being from memory |
− | some_being:destroy() --identical to being | + | some_being:destroy() --identical to being.destroy(some_being) |
</source> | </source> | ||
Line 175: | Line 175: | ||
name = "exploder" | name = "exploder" | ||
.... | .... | ||
− | OnDie = function(self | + | OnDie = function(self) --engine hook upon death |
position = self:get_position() --position for being is found | position = self:get_position() --position for being is found | ||
Level.explosion(position, 3, 30, 5, 5, YELLOW, "", DAMAGE_FIRE) --explosion occurs on the map | Level.explosion(position, 3, 30, 5, 5, YELLOW, "", DAMAGE_FIRE) --explosion occurs on the map |
Latest revision as of 14:53, 10 October 2011
Objects in DoomRL are based on object-oriented programming, which allows for very intracite and elegant systems of functions and variables to work in tandem. Practically everything in DoomRL, whether it be items and enemies, or the level itself, or even the player classes, is based in its particular object class. This section of the tutorial focuses on understanding this core part of the customization process: creating and modifying the game's objects.
Contents |
Introducing Tables
A very large part of customizing the objects in DoomRL (weapons, powerups, enemies, etc) has to do with creating, manipulating, and referencing lua tables. All objects in the game are associated with some table or tables: they hold a number of fields, each of which contain a key (the variable) and a value. The following is an example of a very simple table:
Dude = { name = "John Romero", type = "Awesome guy", }
Adding this to a lua script would create a table called "Dude" with two fields: the key name, which is set to the value 'John Romero'; and the key type, which is set to the value 'Awesome guy'. (You can use numbers for keys as well.) If we want to reference the values in this table, we can use the following format:
Dude.name --gives the value "John Romero" Dude.type --gives the value "Awesome guy" Dude["name"] --identical to Dude.name
While you can create tables yourself, more often than not you will be creating tables based on a specific class. (A class is like a variable type, except it can carry a vast amount of variables, tables, and functions within it. You just need to know how to work with them so there's no reason to go into detail.) These classes are based on the game's core objects and include "Beings", "Items", and "Missiles". To create a table using a class, do not use an equal sign.
Beings{ name = "super enemy", ascii = "S" .... }
The result is that a new table called "super enemy" will exist as a part of the Beings class.
There are four important parts to understand when working with classes in DoomRL modding:
- The prototype is the game object's mold: the game uses it to make as many objects as necessary. Whenever you create a new object, you will do so by creating the prototype.
- The properties are the variables in a particular object that can be modified for said object. They are distinct from prototype: changing values in the prototype affects ALL objects, while changing properties affects only the object in your scope.
- The engine hooks are ways to execute code when particular events occur for an object. Hooks are why phase devices teleport you around when you use them (with an OnUse hook) and why shamblers regenerate their health every turn (with an OnAct hook).
- The API is a set of functions that lets you interact with various pieces of the DoomRL engine. Often this gives you an easier way to change the prototype after initializing it, although there are a wide-ranging collection of functions for each class that allow for interesting game possibilities.
We will cover all of these in the following sections.
Prototypes
Every game object starts with its prototype. It is meant to be a stable definition of an object from which various properties of an instantiated object can be used and modified. We will begin by looking at the basic keys used to designate an Item object prototype (full documentation can be found here).
Items{ name = "a thing", --name of the item, as viewed in-game id = "thing", --item's in-script reference name (defaults to name) color = LIGHTGRAY, --color of item, as viewed in-game level = 1, --minimum in-game floor on which that item can appear weight = 1000, --weighted likelihood of item spawning type = ITEMTYPE_NONE, --type of item }
You should notice that, in addition to the normal value types, there are special values used for color and type. Very often there are a number of pre-defined values by the game and can be found in full in the constants and flags page, which should be used in some of the prototype fields. In particular, color can be any of sixteen colors (e.g., LIGHTGRAY) and type can be any of twelve particular item settings. Most often you will see types such as ITEMTYPE_RANGED, ITEMTYPE_PACK, and ITEMTYPE_POWER used, but you should take a look at all of them to see what best suits your need for the particular item.
ITEMTYPE_RANGED, for instance, contains the following additional prototype keys:
Items{ name 'big gun', .... type = ITEMTYPE_RANGED, desc = "It's a really big gun.", --in-game description, as viewed in the inventory ascii = "}", --single character representing the weapon, as viewed in-game damage = "4d4", --weapon's damage: should be written as 'XdY' ala dice notation damagetype = DAMAGE_BULLET, --weapon's damage type group = "weapon-biggun", --weapon's category: mainly for score-keeping ammoID = "bigbullet" --ammo that weapon uses: use the id of that ammo here fire = 10, --how quickly the weapon fires, in turns (1 second = 10 turns) acc = 3, --how accurate the weapon is ammomax = 50, --weapon's clip size radius = 0, --size of shot's explosion radius shot = 5, --how many shots the weapon fires shotcost = 1, --how much ammo each shot takes missile = "big_shot", --what the projectile of the weapon is }
For the desc key, since the string itself contains a single quote, we can wrap the string around double-quotes: lua handles this without error. Alternatively, we could use a backslash (such as \') to add otherwise-unusable characters in strings.
There are also some additional keys for alt-fires and alt-reloads, but these are the most important for setting up a ranged weapon. Notice that the last key requires an identifier from a missile object: while there are a number of pre-set missiles from which you can choose, you can also create your own missile prototype and use the id key from that prototype for the missile (the above example would require a missile object with the id of 'big_shot'). Additionally, if the missile you design is unique to this weapon, you can define the prototype directly in the weapon, like so:
Items{ name 'big gun', .... missile = Missiles{ --assign missile prototype .... --missile prototype keys go here } --missile prototype completes on this line } --item prototype completes on this line
In creating the missile this way, you can omit the identifier of the missile, as it is automatically created for you (and should not need to be called anyway).
Referencing and changing prototype values is a more difficult than changing properties (see below), and potentially dangerous depending on what you are trying to do. It is quite possible, however, by first referencing the class that the prototype belongs to, followed by the id of the prototype, followed by the key whose value you wish to reference/change:
local new_damage = "3d3" --create a damage variable items.pistol.damage = new_damage --change the damage key of the pistol prototype
Executing this code will change the damage of every pistol object in the game. In general, this is not recommended for use with any code that occurs after the game has already started.
Properties
An object's properties are its own unique set of characteristics that set it apart from any other object in the game, including other copies of the same object. An object will initially receieve its property values from the object's prototype on creation, but they can be changed constantly after this without affecting other object instances.
One easy object that can have its properties manipulated without worrying about other instances is the player. As there is only ever one player in the game at any time, it is much easier to simply manipulate property values rather than digging into the prototype. The player has properties from both the player object and the being object, since the player is actually a sub-class of beings. If we wanted to change the player's health and experience level, we would do it in the following way:
player.hp = player.hp + 10 --increases player's HP by 10 player.explevel = 5 --sets player's level to 5
Note that the health property is hp, not HP: while this is just a case-sensitive example, sometimes the key of a property field representing a prototype key can be very different (beings have the XP field for how much experience they give, but an expvalue for their corresponding property). Be aware of these differences when coding.
Properties must always refer to a specific instance of an object. While it may seem bothersome to have to point to a specific object every time you want to change a property, these are automatically handled by engine hooks and the API, as we will see shortly.
Engine Hooks
Engine hooks are functions that allow you to truly customize object interaction. They are associated with specific events in the DoomRL engine and essentially allow for input of anything you want to occur during those events.
Like prototype keys, engine hooks are added during the initialization process of the object. They should be placed at the end of all the prototype keys, and are written much like functions:
Beings{ name = "nasty", .... --other prototype keys are added OnAction = function(being) --engine hook is called .... --code to be executed in the hook goes here end, --function ends } --prototype ends
The following shows a being called "nasty" getting initialized into DoomRL and, with it, the hook OnAction(). Hooks work just like functions do, carrying input arguments and a potential output return value. OnAction() has a being input argument, which is used as a reference to the being whenever OnAction() triggers for that particular being; OnAction() has a void output argument, meaning that you cannot return a value using this hook. (Note that, for hooks with output arguments, the return value affects an outcome on the engine side rather than a usable value in the code.) The OnAction() hook itself is triggered whenever a being carries out an action (usually once per second), and so will be constantly called over the course of the being's lifetime.
Hooks are applied to a particular instance of an object, not the prototype itself. This means that, if you're changing, for instance, the HP value of a being, you must call the being's hp property rather than its prototype key. What makes the input argument so useful is that it automatically refers to whatever being activated the hook, rather than you needing to specify exactly which being needs to call the function. Thus, if we want a being object that regenerates its health on every action (like the shambler), we would write the following code:
Beings{ name = "regenerator", .... OnAction = function(being) --initialize the hook if being.hp < being.hpmax --prevent being from healing past maximum health being.hp = being.hp + 1 --being gains one HP end --end of conditional statement end, --end of engine hook }
First, we check to see if the being instance's health is less than its maximum health: if this is true, we raise the hp property of the instance by one; if not, nothing happens. Once the if statement finishes, there is nothing left for the function to do, and so it ends as well. OnAction() will make it so that the if statement is called every time the being instance acts, so it could be hard to kill something like this! (Anyone trying to kill shambler with a shotgun should understand well enough.)
Most of the engine hooks are found with the item and level object classes, since there are a number of events that the engine specifically checks for these objects over the course of their lifetimes. Common hooks are OnCreate(), OnEnter(), and OnDestroy()/OnDie() (which practically do the same thing, just for different object classes).
API
Object API is very simple in DoomRL: for each object class, there is a set of pre-defined functions, called methods, that allow modders to manipulate objects of that class. Most, if not all, of the methods in an object's API are otherwise impossible to create within lua itself, which is why we are lucky to have them available. Unlike functions, however, you need not define methods as such: simply writing the functional expression itself (without preceding it with "local function", for example) is all that is necessary.
In lua, the convention for methods is to use a period between the class name and the method name. If the first argument requires a particular instance of an object in that class, one can use shorthand by writing out the object instance, followed by a colon, then the method name, at which point the first argument is automatically used in the method without having to explicitly write it.
being.new(id) --this will allocate a new instance of the being object id to memory being.destroy(some_being) --this will deallocate a being instance called some_being from memory some_being:destroy() --identical to being.destroy(some_being)
Whenever you are not working with a particular instance of an object, it usually means you are creating a new one. For some objects, this is a very important use. However, most of the API is meant to interact with specific object instances, so we will take a look at an example for such a method.
Let us suppose we want to make an enemy explode whenever it dies. One way to do this is the following:
Beings{ name = "exploder" .... OnDie = function(self) --engine hook upon death position = self:get_position() --position for being is found Level.explosion(position, 3, 30, 5, 5, YELLOW, "", DAMAGE_FIRE) --explosion occurs on the map end, }
Here we use the OnDie() hook to execute code whenever an "exploder" being dies. Creating an explosion on the map is done with the method Level.explosion(), which carries quite a few input arguments: coordinates on the map where explosion should happen, radius of explosion, millisecond delay on explosion's path, damage dice, side of damage dice, color of explosion, sound id for explosion, and damage type of explosion. (Note that there are even more arguments, but these can be safely ignored if they appear after all the arguments we end up using.) The coordinates of the being instance are not immediately accessible in its properties, but can be found with the thing:get_position() method (where the Thing in this case is the being instance, or self, defined inside of the OnDie hook). In order to get the coordinates, we assign an output argument position to thing:get_position, and use it in Level.explosion when we need to define them.
Review
As a final, rather large example, we will take a look at one of the enemies from the HereticRL mod (though this exact code might be outdated at some point), the undead warrior. The undead warrior is a fairly straight-forward enemy with the unique twist in that it will occasionally throw red axes (as opposed to green ones), which deal much more damage. To start, we will first need to initialize the two axe weapons: since the undead warrior will have more than one weapon, we cannot inline them within the being definition.
Items{ name = "nat_undeadgreen", --this is the green axe weapon id = "nat_undeadgreen", type = ITEMTYPE_NRANGED, --natural ranged-weapon type damage = "2d6", damagetype = DAMAGE_BULLET, fire = 10, missile = { --in-line missile definition soundID = "undead", ascii = "x", color = LIGHTGREEN, delay = 80, miss_base = 10, miss_dist = 5, }, weight = 0, --don't want these being generated! flags = {IF_NODROP, IF_NOAMMO}, --these are standard flags for NRANGED items } Items{ name = "nat_undeadred", --this is the red axe weapon id = "nat_undeadred", type = ITEMTYPE_NRANGED, damage = "6d2", --better damage than green axe (on average) damagetype = DAMAGE_PLASMA, --stronger damage type fire = 10, missile = { --in-line missile definition soundID = "undead", ascii = "x", color = LIGHTRED, --different color delay = 50, miss_base = 10, miss_dist = 3, --misses less }, weight = 0, flags = {IF_NODROP, IF_NOAMMO}, }
These must be placed in the code before we define the undead warrior itself, as they are called within the being prototype.
Beings{ name = "undead warrior", name_plural = "undead warriors", --this would work as such by default id = "undead", --small ids are nice for calls ascii = "u", color = LIGHTGRAY, sprite = 0, --necessary, always set to 0 for now corpse = true, --whether being leaves a corpse toDam = 6, --inherent damage modifier (for melee) toHit = 4, --inherent accuracy modifier speed = 100, --time of actions: 100 = 1s; <100 = >1s; >100 = <1s armor = 2, --protection value danger = 6, --weight for generation, can also determine experience XP = 0, --set experience specially weight = 250, minLev = 7, --lowest floor being will generate maxLev = 20, --highest floor being will generate HP = 50, attackchance = 60, --chance being will attack as its action toHitMelee = 0, --inherent accuracy modifier (for melee, adds to toHit) res_fire = 50, --percentage resistance to damage type DAMAGE_FIRE res_acid = 50, ai_type = "natural_melee_ranged_ai", --how the enemy moves/reacts, can define yourself desc = "As part of the Order's insidious plot to control your world, they've recruited the dead, " .. "gave them armor and armed them with deadly magic axes. Now they guard the evil cities and " .. "toss their infinite supply of axes at any elf who passes by.", kill_desc = "took an axe to the face from an undead warrior", --shows up in mortem if you died to this being kill_desc_melee = "chopped in two by an undead warrior", --same as above but by melee attack OnCreate = function(self) self.eq.weapon = item.new("nat_undeadgreen") end, OnAction = function(self) if(math.random(6) == 1) then self.eq.weapon = item.new("nat_undeadred") else self.eq.weapon = item.new("nat_undeadgreen") end end, }
Typically, for an enemy that has a ranged attack, you would use the prototype field weapon and in-line an item definition with type ITEMTYPE_NRANGED. However, since the undead warrior has two different attacks, we choose to assign it two separate weapons over the course of its lifetime through engine hooks.
- Whenever an undead warrior is created, it is given a green axe in its weapon equipment slot: this is done by assigning a new item to the weapon field of the equipment property with an OnCreate() hook.
- On every action there's a chance for the undead warrior to get a red axe in its weapon equipment slot.
- First we call the OnAction() hook to make a check on every action.
- Next, we supply a random condition using math.random(num) (which randomly chooses an integer between 1 and num). if(math.random(6) == 1) produces a 1/6 chance of the condition being true.
- If the condition becomes true, we create a new item (this time a red axe) to the same field as before.
- If the condition is false, we put a new green axe there instead. This is to ensure that the red axe is only in the undead warrior's equipment slot 1/6 of the time.
And that's all there is to it! You'll want to change the soundID in the missile definitions, but otherwise this code should run in any module you create.