|
|
(21 intermediate revisions by 3 users not shown) |
Line 1: |
Line 1: |
− | {{Incomplete}}
| |
| Welcome to the DoomRL modules tutorial. The objective of this guide is to explain how to develop your own modules for DoomRL as easily and as accurately as possible: therefore, its scope is fairly limited and will only cover basic techniques in customizing objects, maps, and game events. However, it is written at the level of one who has little to no background knowledge of programming languages, and so is ideal for a player who doesn't understand the fundamentals of scripting. Anyone who has the ambition to create a unique and enjoyable game out of the DoomRL engine should use this as a tool to get started. | | Welcome to the DoomRL modules tutorial. The objective of this guide is to explain how to develop your own modules for DoomRL as easily and as accurately as possible: therefore, its scope is fairly limited and will only cover basic techniques in customizing objects, maps, and game events. However, it is written at the level of one who has little to no background knowledge of programming languages, and so is ideal for a player who doesn't understand the fundamentals of scripting. Anyone who has the ambition to create a unique and enjoyable game out of the DoomRL engine should use this as a tool to get started. |
| | | |
| Modifying DoomRL content is done through lua, a scripting language designed to communicate with a variety of other computer languages. (You can find out more about lua by following [http://www.lua.org/ this link].) As such, a great deal of what can be done for the purpose of DoomRL modding is handled by the API, or application programming interface, which allows a modder to issue commands that are sent to the DoomRL engine (written in Pascal). The formatting and syntax is relatively simple and flexible, so there are a number of ways to write code in lua that will produce the same outcome. We will use a particular format, although alternatives will be explained as they come up. | | Modifying DoomRL content is done through lua, a scripting language designed to communicate with a variety of other computer languages. (You can find out more about lua by following [http://www.lua.org/ this link].) As such, a great deal of what can be done for the purpose of DoomRL modding is handled by the API, or application programming interface, which allows a modder to issue commands that are sent to the DoomRL engine (written in Pascal). The formatting and syntax is relatively simple and flexible, so there are a number of ways to write code in lua that will produce the same outcome. We will use a particular format, although alternatives will be explained as they come up. |
| + | |
| + | Writing lua code yourself is done in a text editor. What text editor you use depends on your operating system: for Windows, it is Notepad. It is suggested that you use a more advanced text editor that handles formatting more effectively. One such text editor (that is free) is [http://notepad-plus-plus.org/ NotePad++], which includes a number of options for easy-to-read and easy-to-write code. Creating a lua file in this manner is as simple as renaming the file with a .lua extension (rather than .txt, for example): NotePad++ itself can save its text files as lua scripts. |
| | | |
| Whenever example code is used in this tutorial, it will be displayed using a special formatting, as shown below: | | Whenever example code is used in this tutorial, it will be displayed using a special formatting, as shown below: |
Line 16: |
Line 17: |
| Much of the tutorial is written in this format, as it is far easier to diplay lua without the default formatting of Wikimedia getting in the way. | | Much of the tutorial is written in this format, as it is far easier to diplay lua without the default formatting of Wikimedia getting in the way. |
| | | |
− | ==Basic Syntax and Definitions==
| + | '''Current tutorials:''' (these have yet to be updated for v0.9.9.5) |
| | | |
− | The following is a very quick explanation of general programming syntax in lua. If you know even a little about programming, you do not likely need to read any of this.
| + | *[[Modding:Tutorial/Basic Syntax and Definitions|Basic Syntax and Definitions]] |
− | | + | *Customization |
− | Writing in a computer language is very brief and exact, and never involves any connotations or implications. In fact, with the exception of particular inputs and outputs, all of the language is entirely internal, purposed to communicate with the computer in order to perform calculations. The basis of these calculations rests on binary relations: true (1) and false (0). If you have a routine that should only be performed until certain conditions, for example, you will want to use binary relations to determine whether or not the routine is run.
| + | **[[Modding:Tutorial/Game Objects|Game Objects]] |
− | | + | **[[Modding:Tutorial/Custom Items|Custom Items]] |
− | The most basic relations are simple comparisons with numbers.
| + | **[[Modding:Tutorial/Custom Beings|Custom Beings]] |
− | | + | **[[Modding:Tutorial/Designing AI|Designing AI]] (incomplete) |
− | <source lang="lua">
| + | |
− | 3 == 3 --true (note that equality relations are understood with ==)
| + | |
− | 12 == 1 --false
| + | |
− | 6 >= 2 --true
| + | |
− | 6 <= 2 --false
| + | |
− | </source>
| + | |
− | | + | |
− | You can also use arithmetic signs (+, -, *, /) with numbers: any arithmetic operators will be done before checking the relational operator.
| + | |
− | | + | |
− | Rather than directly using numbers, you will likely want to assign them to a variable. In lua, declaring a variable can be done by preceding it with the word "local", and assigning the variable a value is done using the equal sign.
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | local yes = 1
| + | |
− | local no = 0
| + | |
− | </source>
| + | |
− | | + | |
− | With this code, whenever you would use the variable 'yes', it will be read as a true value, and whenever you would use the variable 'no', it will be read as a false value. (Note that the parameters are not always "local": this will be covered more in detail later.) Variables can be reassigned as much as you want unless they are read-only on the engine side (this comes up in a few places for our purposes).
| + | |
− | | + | |
− | Variables in lua can be of a variety of different types. Typically you will use the following:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | local var_void = nil --singular value, used mostly to be a placeholder
| + | |
− | local var_boolean = true --double value, true/false meant for satisfying conditions
| + | |
− | local var_number = 3.14159 --double-precision floating-point value, pretty much any number you could possibly want
| + | |
− | local var_string = 'awesome' --sequence of characters, must have single- or double-quotes around them
| + | |
− | local var_function = print() --subroutine, used to reference pieces of code over an entire file
| + | |
− | local var_table = {1 = 'a'} --associative array of fields, holds a lot of information in a single place (explained later)
| + | |
− | </source>
| + | |
− | | + | |
− | For the purpose of modding, you are often restricted by what type you can use based on what is allowed by the API. If an input was expecting a string and you use a boolean value, you will almost certainly get an error. Note that the left side could be just about anything you want: use whatever naming scheme works for you. (It is unadvised that you use "reserved words" for variables: we will see a few just below.)
| + | |
− | | + | |
− | Trying to determine how your code will run is primarily executed through conditional statements. These include:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | if condition --check to see if condition is true
| + | |
− | then result --if condition was true, do result
| + | |
− | end --conditional statements must always finish with this line
| + | |
− | | + | |
− | if condition1 --check to see if condition1 is true
| + | |
− | then result1 --if condition1 was true, do result1
| + | |
− | elseif condition2 --if condition1 was false, check to see if condition2 is true
| + | |
− | then result2 --if condition2 was true, do result2
| + | |
− | else
| + | |
− | result3 --if condition2 (and condition1) was false, do result3
| + | |
− | end
| + | |
− | | + | |
− | while condition --check if condition is true
| + | |
− | do task --if condition was true, task is executed; if it was false, skip
| + | |
− | end --if condition was true, repeat the statement; if it was false, end statement
| + | |
− | | + | |
− | repeat --continue the following lines indefinitely
| + | |
− | task --task executes
| + | |
− | until condition --if condition is true, end statement
| + | |
− | | + | |
− | for var=val1,val2,val3 --iterate following lines based on vals
| + | |
− | do task --task is executed a number of times equal to the number of vals
| + | |
− | end
| + | |
− | | + | |
− | for var=first,step,last --iterate following lines from first to last, using step as iterator
| + | |
− | do task --task is executed (last-first+1)/step times (rounded down)
| + | |
− | end
| + | |
− | | + | |
− | for var,iter in ipairs(array) --iterate following lines for all fields in array, iter acts as index (if necessary)
| + | |
− | do task --task is executed for as many fields as there are in the array
| + | |
− | end
| + | |
− | | + | |
− | for var,iter in pairs(table) --same as above but with a table
| + | |
− | do task --task is executed for as many keys as there are in the table
| + | |
− | end
| + | |
− | </source>
| + | |
− | | + | |
− | If a lot of these are confusing, don't worry as we'll eventually manage to find ways to include them in the tutorial as we go. Finally, we have two miscellaneous statements: break and return. Break will prematurely exit a conditional statement, and could be used to, for instance, exit a "while true" loop (although this should probably be avoided). Return is used with functions and assigns a value that can be used once the function completes. In particular, return comes in handy when we deal with engine hooks.
| + | |
− | | + | |
− | ==Game Objects==
| + | |
− | | + | |
− | 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 a particular table: 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:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | Dude = {
| + | |
− | name = 'John Romero'
| + | |
− | type = 'Awesome guy'
| + | |
− | }
| + | |
− | </source>
| + | |
− | | + | |
− | 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:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | Dude.name --gives the value 'John Romero'
| + | |
− | Dude.type --gives the value 'Awesome guy'
| + | |
− | | + | |
− | Dude[name] --same as Dude.name
| + | |
− | </source>
| + | |
− | | + | |
− | 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.
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | Beings{
| + | |
− | name = "super enemy"
| + | |
− | ascii = "S"
| + | |
− | ...
| + | |
− | }
| + | |
− | </source>
| + | |
− | | + | |
− | 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.
| + | |
− | | + | |
− | ===Prototype===
| + | |
− | 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 [[Modding:Item|here]]).
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | 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
| + | |
− | }
| + | |
− | </source>
| + | |
− | | + | |
− | 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 [[Modding:Constants|constants and flags]] page, which should be used in some of the prototype fields. In particular, color can be [[Modding:Constants#Colors|any of sixteen colors]] (e.g., LIGHTGRAY) and type can be any of [[Modding:Constants#ItemType|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:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | 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
| + | |
− | }
| + | |
− | </source>
| + | |
− | | + | |
− | For the desc key, since the string itself contains a single quote, we wrap the string around double-quotes instead: lua handles this without error.
| + | |
− | | + | |
− | 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 id from a missile object: while there are a number of pre-set missiles from which you can choose, you can also [[Modding:Missile|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:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | 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
| + | |
− | </source>
| + | |
− | | + | |
− | In creating the missile this way, you can omit the id of the missile, as it is automatically created for you (and should not need to be called anyway).
| + | |
− | | + | |
− | Referencing and changing prototype values is potentially hazardous compared to and more difficult than changing properties (see appropriate section). 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:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | local new_damage = '3d3' --create a damage variable
| + | |
− | items.pistol.damage = new_damage --change the damage key of the pistol prototype
| + | |
− | </source>
| + | |
− | | + | |
− | Executing this code will change the damage of every pistol object in the game. In general, this is not recommended to be used for 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 with which we can manipulate its properties 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 [[Modding:Player|player object]] and the [[Modding:Being|being object]], since the player object is actually a sub-class of beings. If we wanted to change the player's HP and level, we would do it in the following way:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | player.hp = player.hp + 10 --increases player's HP by 10
| + | |
− | player.explevel = 5 --sets player's level to 5
| + | |
− | </source>
| + | |
− | | + | |
− | Note that the HP 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 what allow you to truly customize how objects will work. They work with the DoomRL engine and essentially allow you input whatever commands you want at specific points once the object has been created (including the creation itself).
| + | |
− | | + | |
− | 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 like functions:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | Beings{
| + | |
− | name = "nasty"
| + | |
− | ... --other prototype keys are added
| + | |
− | local function = OnAction(self) --engine hook is called
| + | |
− | ... --code to be executed in the hook goes here
| + | |
− | end --function ends
| + | |
− | } --prototype ends
| + | |
− | </source>
| + | |
− | | + | |
− | 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 "self" input argument, which is used as a reference to the being itself whenever OnAction is called; OnAction has a void output argument, meaning that you cannot return a value using this hook. The OnAction function itself is called 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:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | Beings{
| + | |
− | name = "regenerator"
| + | |
− | ....
| + | |
− | local function = OnAction(self) --initialize the hook
| + | |
− | if self.hp < self.hpmax --prevent being from healing past maximum health
| + | |
− | self.hp = self.hp + 1 --being gains one HP
| + | |
− | end --end of conditional statement
| + | |
− | end --end of engine hook
| + | |
− | }
| + | |
− | </source>
| + | |
− | | + | |
− | First, we check to see if the being instance's HP is less than its maximum HP: 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 that allow modders to manipulate an object (or a particular instance) and anything with which it may currently be interacting. Most, if not all, of the functions in an object's API are otherwise impossible within lua itself, which is why we are lucky to have them available. Unlike functions, however, you need not define them as such: simply writing the functional expression itself (without prepending "local function") is satisfactory.
| + | |
− | | + | |
− | There are two kinds of functions in a given API: those that access the object's prototype and those that access an instance of the object. Object prototype functions are called using a period and are written with a lowercase class name, and object instance functions are called using a colon and are written with a capitalized class name.
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | being.new(id) --this will create a new instance of the being object associated with id
| + | |
− | Being:destroy() --this will destroy a specific being object
| + | |
− | </source>
| + | |
− | | + | |
− | Almost all API functions that work with an object's prototype will generate a new instance of said object: for [[Modding:Coord|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 function.
| + | |
− | | + | |
− | Let us suppose we want to make an enemy explode whenever it dies. One way to do this is the following:
| + | |
− | | + | |
− | <source lang="lua">
| + | |
− | Beings{
| + | |
− | name = "exploder"
| + | |
− | ....
| + | |
− | local function = OnDie(self,true) --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
| + | |
− | }
| + | |
− | </source>
| + | |
| | | |
− | Here we use the OnDie hook to execute code whenever an "exploder" being dies. Creating an explosion on the map is done with the function 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 accessable in its properties, but can be found with the Thing:get_position() function (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 the function, and use it in the Level.explosion function when we need to define them.
| + | *Example Modules: Singular |
| + | **[[Modding:Tutorial/Constructing a Map|Constructing a Map]] (the basics) |
| + | **[[Modding:Tutorial/Recreating Hell's Arena|Recreating Hell's Arena]] |
| + | **[[Modding:Tutorial/The Infinite Arena|The Infinite Arena]] |
| + | *Example Modules: Episodic |
| + | **[[Modding:Tutorial/Building an Episode|Building an Episode]] (the basics) |
Welcome to the DoomRL modules tutorial. The objective of this guide is to explain how to develop your own modules for DoomRL as easily and as accurately as possible: therefore, its scope is fairly limited and will only cover basic techniques in customizing objects, maps, and game events. However, it is written at the level of one who has little to no background knowledge of programming languages, and so is ideal for a player who doesn't understand the fundamentals of scripting. Anyone who has the ambition to create a unique and enjoyable game out of the DoomRL engine should use this as a tool to get started.
Modifying DoomRL content is done through lua, a scripting language designed to communicate with a variety of other computer languages. (You can find out more about lua by following this link.) As such, a great deal of what can be done for the purpose of DoomRL modding is handled by the API, or application programming interface, which allows a modder to issue commands that are sent to the DoomRL engine (written in Pascal). The formatting and syntax is relatively simple and flexible, so there are a number of ways to write code in lua that will produce the same outcome. We will use a particular format, although alternatives will be explained as they come up.
Writing lua code yourself is done in a text editor. What text editor you use depends on your operating system: for Windows, it is Notepad. It is suggested that you use a more advanced text editor that handles formatting more effectively. One such text editor (that is free) is NotePad++, which includes a number of options for easy-to-read and easy-to-write code. Creating a lua file in this manner is as simple as renaming the file with a .lua extension (rather than .txt, for example): NotePad++ itself can save its text files as lua scripts.
Whenever example code is used in this tutorial, it will be displayed using a special formatting, as shown below:
Much of the tutorial is written in this format, as it is far easier to diplay lua without the default formatting of Wikimedia getting in the way.