Skip to content

175. 3D Dungeon – Programming AI behaviour

October 20, 2014

Most of the objects I put in the dungeon will interact with the player. Enemies will detect, follow and attack the player. Boxes will offer their contents to the player. Weapons will be collected and damage the enemies in various ways.

All of this activity has to be programmed, and it needs to be flexible, so I can change an object’s behaviour easily, or add new objects without messing with existing code.

The first part of this post explains the need for classes and inheritance, and the second half talks about programming specific behaviour. I’m writing for many different audiences here, so please skip to whatever interests you.

In particular, if you’re fairly new, this is mainly for you.

Classes

I could just write an enormous draw function, which has code for each object, checking if it is in range of the player, and if it is, doing whatever it does. But you can imagine this is going to become a nightmare, especially if we have multiple copies of some objects.

Much better is to put each object’s code in its own class, then it is really easy to add new objects. But the code will be very similar between classes, eg checking for collisions. So how do I share some of the code, while allowing each object to vary it if necessary?

Inheritance

This is a common programming problem, and there is a solution – inheritance. The best way to explain – and show how Codea does it – is with an example.

Every 3D object will start by storing its position and the 3D model used to draw it. It will provide other information, but let’s just look at these two things.

Suppose I have a Spider class. The code would look something like this..

Spider=class()

function Spider:init(model,position)
    self.model=model 
    self.pos=position  
end

--plus functions for drawing, collisions, attacking etc

 

Then as I add more objects, I find the code in the init function is almost identical for all of them. A good programming rule is never to duplicate code, so how I can I deal with this?

I’ll create a “base” class that will handle it.

Base=class()  
function Base:init(model,position) 
    self.model=model
    self.pos=position
end

 

So now I have a “base” class that stores the model and position information.

Now, if I tell the Spider class to “inherit” from the base class, Codea copies all the base class code into the Spider class for me. So I will automatically have an init function with all the code I need.

This means I can reduce my Spider code to this

Spider=class(Base) --Spider inherits from Base

--[no init function needed any more]
--functions for drawing, collisions, attacking etc

 

If I need to change the init function, I just change it in the Base class, and it will automatically copy to all the object classes.

 

But objects vary…

Now things are never quite that simple. Some objects have additional information to pass to the init function, and it needs to be stored. How can I share some, but not all, of  the code and parameters in the init function?

First, instead of naming the parameters specifically, I’ll use a table (explained here). This means I can pass whatever I want to the init function. But how do I store all the different things I might want to pass in for different objects?

There are (at least) two ways to handle this.

One is to write replacement init functions for objects which need extra code to handle other parameters. (If you write an init function in the Spider class, it will replace [“override”] the init function copied in from the Base class). But if I do this, I will be duplicating the model and position code from the Base class, which is why I created a Base class to begin with. I really want a solution that only adds code for the extra bits needed by each class.

So what I did was very simple.

--arg is a table of named parameters 
--eg {model="Spider",position=vec3(10,40,10)}
function Base:init(arg) 
    self.model=arg.model
    self.pos=arg.position
    self:setup(arg)  --<-----NEW
end

function Base:setup(arg)  --<---NEW
end

At the end of the Base init function, I run a function called setup, and pass it the table of parameters. The setup function doesn’t have any code – that will be added by object classes.

So now suppose that the Spider class has an extra parameter called speed. This is how the Spider class will store it, using the setup function.

Spider=class(Base) --Spider inherits from Base

function Spider:setup(arg)
    self.speed=arg.speed
end

--functions for drawing, collisions, attacking etc

What is happening is that the init function from the Base class runs, storing model and position, and then it runs the setup function (which will be the setup function I wrote in Spider) and stores speed. So I can use the standard init function from Base, and add any extra code I need in the setup function.

Programming behaviour

I’ve used the same technique as for my 2D side scroller, which is that code for an object’s behaviour, ie not just moving around, but detecting the player, chasing the player, colliding with the player, etc, is stored in the object class.

The main draw class simply loops through all the objects and runs their draw functions – which makes them pretty important.

As with the init function, I’ve put everything I can, into the Base class. This makes my object code very compact. For example, here is the (almost) complete code for my zombie.

Zombie=class(Base)

function Zombie:setup(arg)
    self.speed=arg.speed or 0
end

function Zombie:collide(playerPos,detectionRange)
    if self:HasCollided(playerPos) then
        --TO DO -- inflict damage on health
    elseif self:PlayerDetected(playerPos,detectionRange,
                               self.ignoreVisibility) then
        if not self.seen then 
            Fx.Play("brains")
            self.seen=true
        end
        self:Move(playerPos)
    end   
end

function Zombie:draw(playerPos,lightRange)
    self:collide(playerPos,lightRange)
    --make zombie bounce a little
    self.pos.y=self.pos.y+0.15*noise(ElapsedTime) 
    self:DrawModel(playerPos,lightRange)
end

The zombie detects the player when he becomes visible, and chases him, yelling “Brains!”. Because I have to fake the animation, I just jiggle the zombie a bit.

However, the draw function which manages this only has 3 lines of code, and the only other function of any size in the class, for collisions, only has a handful of lines. This is because things like detection, collision, and even moving are common to most or all objects, so I have created Base functions for all of them.

I’m sure you can see how this makes it much easier to create new objects or edit existing objects.

Basic behaviour

Most of the enemies in the dungeon are going to be pretty single minded. If they see you, they will attack.

I started off making my lighting very simple – I set a light radius, and a shader reduces the brightness from 1 at the player, to 0 at the edge of the radius.

This has had many benefits.

  • it is much faster than full scale lighting (ambient, diffuse, specular)
  • it looks good – like a lamp
  • it is easy to decide whether to draw any object (including collision detection etc) by just testing if they are within the light radius (and not blocked by a wall)
  • because an object only chases the player if it can see him, this means there must be no obstacles or walls between them. Therefore I can just start the object moving straight at the player, without worrying about finding a path round obstacles, ie no path finding.

I really like this simplicity, because it is not only going to maximise my frames per second speed, but it gives me lots of room to add more complexity later, if I want.

So my zombie is extremely simple. He sees you and attacks.

The spider is slightly different. He is hanging in mid air, and when he sees you, he comes down to the floor and then comes toward you. The code for this is very simple.

Facing the player

But it’s the little details that are sometime tricky. If an object comes toward you, it needs to face you. This means I need to rotate the object on the y axis.

The first problem is that 3D models are built by other people, and they could be facing in any direction. Fortunately, I thought of this. I set up a test project where I can import models, and rotate them and resize them so they are suitable for importing straight into my dungeon. I’ll post about this separately, but for now, just accept that one of the things I do, is ensure that any model I import is facing forward (the same direction as the game starts).

This means that I know which direction each model is facing, to start with. Then I can use the position of the object and player to work out the angle between them, and use simple trigonometry to rotate the object.

At least that’s the theory. It only took me about 3 or 4 hours to get it working. I never was much good at geometry. I seem to spend a lot of time looking at an empty dungeon, where I should be seeing a zombie.

 Detecting the player

My lighting has made things simpler, but even if an object is within the light radius, it might be on the other side of a wall, and invisible. So I need to check if there is a wall between the player and any object inside the light radius.

I do this by creating a 2D table which stores all the wall positions (so if the tile at column 8, row 7 has a wall, then my table has [8][7] = 1 ).

Then I start at my object and move in small steps (using a for loop) toward the player, calculating what tile I am in at each step, and checking if it is a wall. If I get all the way to the player, then I can see him.

 Weapons, aiming, shooting

I’ll save this for another post. It is not easy.

Nothing is easy in 3D.

I don’t know why I do this to myself.

Leave a Comment

Leave a comment