Modding:Tutorial/Designing AI

From DoomRL Wiki

Revision as of 14:17, 14 October 2011 by Game Hunter (Talk | contribs)

Jump to: navigation, search

Before the more recent versions of DoomRL, all beings other than the player used a single algorithm for AI, or artificial intelligence, that directed how the being should act under certain circumstances. Although this default AI is modifiable to some extent through a few being flags, the extent to which beings acted remained roughly the same. When it became clear that this default AI could be outwitted with clever tactics, the AI object was added into the game (sometimes known as "lua AI"), allowing for complete customizability as to what a being does given any circumstances. Most beings, as of v0.9.9.4, call a particular AI object that governs their actions, and are thus more difficult to deal with as a whole.

This tutorial goes through the basics of creating AI objects and what you can do with them. The AI object class is very flexible in that the basic structure is extremely simple, allowing you to add almost anything that the game allows from within its API. Unsurprisingly, designing your own artificial intelligence can be an extremely challenging task, depending on what exactly you want the AI to be capable of. Basic structuring techniques will be explained, as well as general guidelines when writing the code that your AI will consist of.

Base Prototype

Since the AI object is so very simple in its required fields, all of them will be contained in the following example:

AI{
    name = "simple_ai",
 
    OnCreate = function(self)
        self:add_property("ai_state", "first_state")
        ....
    end,
 
    OnAttacked = function(self)
        ....
    end,
 
    states = {
        first_state = function(self)
            ....
        end,
 
        ....
    }
}
  • name is the identifier of the AI object, to be used as the field value for the being key ai_type.
  • OnCreate(self) triggers whenever self is created. For any being using this AI object, it is functionally identical to that being's OnCreate() hook in its own prototype.
    • The "required" property here, ai_state, is the key factor when determining what subfunction the being should run in the AI object.
  • OnAttacked(self) triggers whenever self is hit. The hit does not have to cause damage. (Such a requirement can be a condition within the hook by comparing HP across multiple actions.)
  • states is an array of functions that are called based on the value of ai_state. These functions will be run immediately after the being's own OnAction() hook in its own prototype. In the above example, since ai_state is set to "first_state" during OnCreate(), the being will run the first_state() function.

Technically speaking, the "state" system of the AI object need not be used: one can instead invoke OnAction(self) directly. However, states are a very practical way to organize the various actions or modes that the AI transitions through, and its system will be used throughout this tutorial. Even including the optional state system, however, what is shown above is all that is necessary for the AI object to initialize.

AI Rules

While you are free to develop your artifical intelligence however you want, there are still some fundamental laws that govern their structure.

Rule 1: The AI must always lower the being's energy. Energy, or scount, is determines when the being can take an action, and so long as its energy remains the same, it can continuously take more and more actions until the game refuses to allow a continuation (ie, infinite loop error). There are a number of API functions that will automatically cause the being's energy to decrease, but when none of these functions are called, you must directly lower the being's scount in the following manner:

self.scount = self.scount - 1000
self.scount = self.scount - 10*self.speed
self.scount = 4000

While these three examples may appear similar, they are quite different. Recall that scount increases by speed each game turn, and decreases by 100 multiplied by the number of game turns for default actions (e.g., moving or attacking). Knowing this, we can see three separate possibilities:

  • The first, reducing scount by 1000, will require a being with speed set to 100 to wait exactly ten game turns. For beings with speed greater than 100, it will occur sooner, and for beings with speed less than 100, it will occur later. Thus, a being with an action modified by this method will act "constant" with respect to its default action speed.
  • The second, reducing scount by 10 multiplied by the being's speed, will require any being, regardless of speed, to wait exactly ten game turns. Thus, a being with an action modified by this method will act "global" with respect to its default action speed (that is, all beings across the global scale take the same amount of time).
  • The third, setting scount to 4000, is functionally identical to the first, except that it can be used immediately after default actions in order to make scount decrease by a very specific value. Suppose you wanted a being to be able to equip things immediately: using this method you can reset the scount value (to 5001) in order for the process to have taken no virtually no time at all. (Technically you could compare scount before and after actions taken, but this is a much simpler method.) Thus, a being with an action modified by this method will act "independent" with respect to its default action speed.

You will almost always use the "typical" modifier, but don't forget about the other two. (In particular, "global" and "independant" modifiers become important when you have several beings using the same AI and want to coordinate their actions very precisely.)

Rule 2: Changing states during an action always requires you to exit the current state. There are two ways to exit a function: break and return. Consequently, there are two ways to switch states:

AI{
    name = "two_state",
 
    OnCreate(self)
        self:add_property("ai_state","state1")
    end
    ....
    states = 
        state1 = function(self)
            return("state2")
        end,
 
        state2 = function(self)
            self.scount = self.scount - 1000
        end,
    end,
}

The return method is a single line that both ends the function prematurely and sets the new ai_state. In the above function, the being begins in state1(), at which point it immediately changes to state2(), then ends in state2() (since scount has decreased when that state ends).

AI{
    name = "two_state",
 
    OnCreate(self)
        self:add_property("ai_state","state1")
    end
    ....
    states = 
        state1 = function(self)
            self.ai_state = "state2"
            break
        end,
 
        state2 = function(self)
            self.scount = self.scount - 1000
        end,
    end,
}

If changing states with the break method, you'll have to establish what state you're changing to first by modifying the "ai_state" property, then follow it with a break. On the surface, this is like the return method with an extra line. However, you can split the functionality when you assign the ai_state and when you break, in case you expect to purposefully want the state to repeat itself (and are reasonably certain that it will not tend toward an infinite loop error). In many cases, however, you should use the return method to immediately switch to another state.

Note that, were there no break or return in state1(), even if the "ai_state" property was modified, the state will continuouslly repeat until an infinite loop error occured. This is unique to the AI object, due to the way that the "ai_state" property is handled.

Personal tools