Difference between revisions of "Modding:Tutorial"

From DoomRL Wiki

Jump to: navigation, search
(started to work on tutorial: I'm not actually near any code so this is all from memory and will be really bad: will edit/add to later.)
 
(more stuff added)
Line 8: Line 8:
 
This is an example sentence.
 
This is an example sentence.
 
All spaces/symbols are shown completely          like so.
 
All spaces/symbols are shown completely          like so.
In addition, html formatting <b>cannot work</b> here.
+
In addition, html formatting <b>cannot</b> work here.
 
However, certain words (for in or and, etc) as well as formats are colored;
 
However, certain words (for in or and, etc) as well as formats are colored;
 
--comments in the language are given using two dashes;
 
--comments in the language are given using two dashes;
Line 18: Line 18:
 
==Basic Syntax and Definitions==
 
==Basic Syntax and Definitions==
  
The following is a very quick explanation of general programming lua syntax. If you know even a little about programming, you do not likely need to read any of this.
+
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.
  
 
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, much 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.
 
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, much 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.
Line 30: Line 30:
 
6 <= 2  --false
 
6 <= 2  --false
 
</source>
 
</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.
 
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.
Line 38: Line 40:
 
</source>
 
</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 (this comes up in a few places).
+
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).
  
A value's type, or class, is usually determined by the API: that is, the values you can assign when modifying DoomRL content usually have to be a particular class, or else they won't work. When a variable needs to be a word or some amount of characters, it is called a string and should be placed in single- or double-quotes. Boolean values can be written either as true/false or as 1/0 (respectively); integers must be a whole number; bytes must be written as an integer between 0 and 15. Internally, you will tend to deal with booleans, integers, and strings exclusively, unless you create your own class.
+
Variables in lua can be of a variety of different types. Typically you will use the following:
  
Trying to determine how your code will run is done through conditional statements. These include:
+
<source lang="lua">
 +
local nil = nil          --singular value, used mostly to be a placeholder
 +
local boolean = true      --double value, true/false meant for satisfying conditions
 +
local number = 3.14159    --double-precision floating-point value, pretty much any number you could possibly want
 +
local string = 'awesome'  --sequence of characters, must have single- or double-quotes around them
 +
local function = print()  --subroutine, used to reference pieces of code over an entire file
 +
local 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">
 
<source lang="lua">
if condition1       --check to see if condition1 is true
+
if condition    --check to see if condition is true
     then result1   --if condition1 was true, do result1
+
    then result --if condition was true, do result
elseif condition2   --if condition1 was false, check to see if condition2 is true
+
end            --conditional statements must always finish with this line
     then result2   --if condition2 was true, do result2
+
 
 +
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
 
else
     result3         --if condition2 was false, do result3
+
     result3       --if condition2 (and condition1) was false, do result3
end                 --conditional statements must always finish with this line
+
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>
 
</source>
  
==Tables==
+
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.
  
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. The following is an example of a very simple table:
+
==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">
 
<source lang="lua">
 
Dude = {
 
Dude = {
     name = 'Game Hunter'
+
     name = 'John Romero'
     type = 'DRL player'
+
     type = 'Awesome guy'
 
}
 
}
 
</source>
 
</source>
  
Adding this to a lua script would create a table called "Dude" with two variables: name, which is set to the value 'Game Hunter'; and type, which is set to the value 'DRL player'. Often variables within tables are called properties, since they are a part of the table and hence a property (e.g., all matter has some amount of mass, therefore mass is a property of matter).
+
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:
  
While you can create tables yourself, more often than not you will be creating tables based on a specific class. 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">
 +
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">
 
<source lang="lua">
 
Beings{
 
Beings{
     id = "enemy"
+
     name = "super enemy"
     ....
+
     ascii = "S"
 +
    ...
 
}
 
}
 
</source>
 
</source>
  
The result is that a new table called "enemy" will exist as a part of the Beings class.
+
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 '''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 '''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 '''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 = type string    --name of the item, as viewed in-game
 +
    id = type string      --object reference name, to be used in other places within the script (defaults to name)
 +
    color = type Color    --color of item, as viewed in-game
 +
    level = type integer  --minimum in-game floor on which that item can appear
 +
    weight = type integer --weighted likelihood of item spawning
 +
    type = type ItemType  --type of item
 +
}
 +
</source>
 +
 
 +
You will notice that, in addition to the normal value types, there is also "Color" and "ItemType". These types are very often pre-defined by the game and can be found in full in the [[Modding:Constants|constants and flags]] page. In particular, Color can be [[Modding:Constants#Colors|any of sixteen colors]] (e.g., LIGHTGRAY) and ItemType 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 = type string          --in-game description, as viewed in the inventory
 +
    ascii = type string          --single character representing the weapon, as viewed in-game
 +
    damage = type string        --weapon's damage: should be written as 'XdY' ala dice notation
 +
    damagetype = type DamageType --weapon's damage type (e.g., bullet)
 +
    group = type string          --weapon's category: mainly for score-keeping (e.g., weapon-chain)
 +
    ammoID = type string        --ammo that weapon uses: use the id of that ammo here (e.g., rocket)
 +
    fire = type integer          --how quickly the weapon fires, in turns (1 second = 10 turns)
 +
    acc = type integer          --how accurate the weapon is
 +
    ammomax = type integer      --weapon's clip size
 +
    radius = type integer        --size of shot's explosion radius: set to 0 if you don't want an explosion
 +
    shot = type integer          --how many shots the weapon fires: defaults to 0 (which gives one shot)
 +
    shotcost = type integer      --how much ammo each shot takes: defaults to 0 (which takes one ammo per shot)
 +
    missile = type MissileID    --what the projectile of the weapon is
 +
}
 +
</source>
 +
 
 +
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 a MissileID: 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.  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).

Revision as of 02:09, 8 September 2011

Under Construction!
This page is still undergoing development by one or few members. It is highly recommended that you do not edit anything here until this tag is removed.

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.

Whenever example code is used in this tutorial, it will be displayed using a special formatting, as shown below:

This is an example sentence.
All spaces/symbols are shown completely          like so.
In addition, html formatting <b>cannot</b> work here.
However, certain words (for in or and, etc) as well as formats are colored;
--comments in the language are given using two dashes;
'and strings are expressed' between "single or double quotes".

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

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.

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, much 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.

The most basic relations are simple comparisons with numbers.

3 == 3  --true (note that equality relations are understood with ==)
12 == 1 --false
6 >= 2  --true
6 <= 2  --false

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.

local yes = 1
local no = 0

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:

local nil = nil           --singular value, used mostly to be a placeholder
local boolean = true      --double value, true/false meant for satisfying conditions
local number = 3.14159    --double-precision floating-point value, pretty much any number you could possibly want
local string = 'awesome'  --sequence of characters, must have single- or double-quotes around them
local function = print()  --subroutine, used to reference pieces of code over an entire file
local table = {1 = 'a'}   --associative array of fields, holds a lot of information in a single place (explained later)

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:

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

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:

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] --same as 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 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 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 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).

Items{
    name = type string    --name of the item, as viewed in-game
    id = type string      --object reference name, to be used in other places within the script (defaults to name)
    color = type Color    --color of item, as viewed in-game
    level = type integer  --minimum in-game floor on which that item can appear
    weight = type integer --weighted likelihood of item spawning
    type = type ItemType  --type of item
}

You will notice that, in addition to the normal value types, there is also "Color" and "ItemType". These types are very often pre-defined by the game and can be found in full in the constants and flags page. In particular, Color can be any of sixteen colors (e.g., LIGHTGRAY) and ItemType 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 = type string           --in-game description, as viewed in the inventory
    ascii = type string          --single character representing the weapon, as viewed in-game
    damage = type string         --weapon's damage: should be written as 'XdY' ala dice notation
    damagetype = type DamageType --weapon's damage type (e.g., bullet)
    group = type string          --weapon's category: mainly for score-keeping (e.g., weapon-chain)
    ammoID = type string         --ammo that weapon uses: use the id of that ammo here (e.g., rocket)
    fire = type integer          --how quickly the weapon fires, in turns (1 second = 10 turns)
    acc = type integer           --how accurate the weapon is
    ammomax = type integer       --weapon's clip size
    radius = type integer        --size of shot's explosion radius: set to 0 if you don't want an explosion
    shot = type integer          --how many shots the weapon fires: defaults to 0 (which gives one shot)
    shotcost = type integer      --how much ammo each shot takes: defaults to 0 (which takes one ammo per shot)
    missile = type MissileID     --what the projectile of the weapon is
}

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 a MissileID: 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. 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 id of the missile, as it is automatically created for you (and should not need to be called anyway).

Personal tools