This is a step-by-step breakdown of how I’ve implemented FNAF-like AI in all of my games since creating Stuffed: Stand-alone.
For the purpose of making this more widely accessible, I will be using lua as the language of choice throughout this tutorial, as it is one of the less “noisy” programming languages, allowing for an easier assessment in understanding the code for a potential beginner.
First and foremost, we begin by defining all the locations that we would like our character to be able to utilize throughout a given night as such:
local LOCATIONS = {
SHOWSTAGE = 1,
DININGAREA = 2,
BATHROOM = 3,
KITCHEN = 4,
EASTHALL = 5,
EASTCORNER = 6,
OFFICE = 7,
}
You can think of these locations as a sort of “state.”
Think in terms of a street light. A street light can only be one color at a time, whether that color is either green, yellow, or red.
in terms of our characters, we will be using these locations to represent the “state” that a character can be at, at any given time.
Once we’ve defined our locations, we must now craft the “blueprint” for creating new characters.
We can do this through utilizing metatables, which is Lua’s way of simulating object oriented programming.
Such a metatable would look something like this:
local Character = {}
Character.__index = Character
function Character.new(location, path)
local init = {
location = location,
path = path
}
setmetatable(init, Character)
return init
end
Now that we’ve created our blueprint, we can instance it. Or, “copy” it.
For this example, we will be creating Chica. So for our particular case, we will instance the blueprint into the “Chica” character.
local Chica = Character.new(LOCATIONS.SHOWSTAGE, {
{LOCATIONS.SHOWSTAGE, LOCATIONS.DININGAREA, LOCATIONS.DININGAREA, LOCATIONS.DININGAREA},
{LOCATIONS.DININGAREA, LOCATIONS.BATHROOM, LOCATIONS.EASTHALL, LOCATIONS.BATHROOM},
{LOCATIONS.BATHROOM, LOCATIONS.DININGAREA, LOCATIONS.KITCHEN, LOCATIONS.KITCHEN},
{LOCATIONS.KITCHEN, LOCATIONS.BATHROOM, LOCATIONS.EASTHALL, LOCATIONS.EASTHALL},
{LOCATIONS.EASTHALL, LOCATIONS.KITCHEN, LOCATIONS.EASTCORNER, LOCATIONS.EASTCORNER},
{LOCATIONS.EASTCORNER, LOCATIONS.EASTHALL, LOCATIONS.OFFICE, LOCATIONS.OFFICE},
{LOCATIONS.OFFICE, LOCATIONS.OFFICE, LOCATIONS.OFFICE, LOCATIONS.OFFICE}
})
You may have taken notice to the rather sizable second argument of this instance.
The path is what is known as a 2d array. it has two indexes, a row and a column, and we will be utilizing both of these things in order to determine both where the character is, as well as the moves they will be allowed to take based on that information.
Now that we have our character, we can move onto actually getting the character around.
We will begin by creating a method of the character metatable. You can think of a method as a characteristic that all "Characters" will share.
Character:move_character()
local move_chance = math.random(1,2)
if move_chance == 2 then
local size = #self.path[1] # self represents the variables of the blueprint
local rand_move = math.random(2, size)
self.location = self.path[self.location][rand_move]
end
end
Line by line, the code says this:
1. each time this function is accessed, store the result of a random number roll
between 1 and 2,
2. is the number 2? (50/50) If so, get the size of our array's width, and do a number between 2, (skipping the first number because it represents our character's location,) and the total horizontal elements in the array
3. move the character by determining their current location, and the random roll that we processed
And done. in almost no loc whatsoever,
we now have a very dynanic system in place for creating and moving characters.
If you were using a proper game engine and/or were using some form of a game loop, you would want to wait a custom amount of time prior to actually doing the roll.
Character:move_character()
if self.time >= some kind of time window
local move_chance = math.random(1,2)
if move_chance == 2 then
local size = #self.path[1]
local rand_move = math.random(2, size)
self.location = self.path[self.location][rand_move]
end
self.time_window = 0
end
Additionaly, you'd want to decrement this time_window as the in-game hour would progress, as well as stop the "movement timer" upon the character reaching the office, but that would go beyond this breakdown's scope.
This is the end result of the code, along with a few minor additions for testing clarity.
local LOCATIONS = {
SHOWSTAGE = 1,
DININGAREA = 2,
BATHROOM = 3,
KITCHEN = 4,
EASTHALL = 5,
EASTCORNER = 6,
OFFICE = 7,
}
local curr_location = {
"SHOWSTAGE",
"DININGAREA",
"BATHROOM",
"KITCHEN",
"EASTHALL",
"EASTCORNER",
"OFFICE",
}
local Character = {}
Character.__index = Character
function Character.new(name, location, path)
local init = {
name = name,
location = location,
path = path
}
setmetatable(init, Character)
return init
end
local Chica = Character.new("Chica", LOCATIONS.SHOWSTAGE, {
{LOCATIONS.SHOWSTAGE, LOCATIONS.DININGAREA, LOCATIONS.DININGAREA, LOCATIONS.DININGAREA},
{LOCATIONS.DININGAREA, LOCATIONS.BATHROOM, LOCATIONS.EASTHALL, LOCATIONS.BATHROOM},
{LOCATIONS.BATHROOM, LOCATIONS.DININGAREA, LOCATIONS.KITCHEN, LOCATIONS.KITCHEN},
{LOCATIONS.KITCHEN, LOCATIONS.BATHROOM, LOCATIONS.EASTHALL, LOCATIONS.EASTHALL},
{LOCATIONS.EASTHALL, LOCATIONS.KITCHEN, LOCATIONS.EASTCORNER, LOCATIONS.EASTCORNER},
{LOCATIONS.EASTCORNER, LOCATIONS.EASTHALL, LOCATIONS.OFFICE, LOCATIONS.OFFICE},
{LOCATIONS.OFFICE, LOCATIONS.OFFICE, LOCATIONS.OFFICE, LOCATIONS.OFFICE}
})
function Character:move_character()
while self.location ~= LOCATIONS.OFFICE do
local move_chance = math.random(1,2)
if move_chance == 2 then
local size = #self.path[1]
local rand_move = math.random(2, size)
self.location = self.path[self.location][rand_move]
print (self.name .. " is now located at: " .. curr_location[self.location])
end
end
end
Chica:move_character()












0 comments