Created
October 23, 2015 13:55
-
-
Save Elmuti/78fa75ee9e12efa2eb2f to your computer and use it in GitHub Desktop.
ai.lua
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
From a high-level perspective, NPCs follow a fairly simple (and real-world logical) process for making decisions. | |
The easiest way to understand it is to examine the basic outline first, and then dig further into the necessary exceptions afterwards. | |
Each time an NPC thinks, it follows this routine: | |
- Perform Sensing - | |
NPC Sensing generates a list of entities that it can see, | |
and another list of NPC sounds that it can hear. | |
The NPC ignores entities and sounds that it doesn't care about. | |
- Generate a list of Conditions - | |
Conditions are key pieces of information that the NPC will be using to make a decision. | |
They are extracted from the sensed lists of entities & sounds, and from the state of the world and the NPC. | |
Conditions might include: | |
- I can see an enemy | |
- I have taken some damage | |
- My weapon's clip is empty | |
- Choose an appropriate State - | |
The State is the overall assessment of the NPC based upon the list of Conditions. | |
For example: | |
- NPCs with a visible enemy enter Combat | |
- NPCs who have no enemies left will drop back to Alert | |
- NPCs with a health of 0 would move to Dead | |
- Select a new Schedule if appropriate - | |
The Schedule is the overall action being taken by the NPC, which is then broken down into Tasks (see below) | |
for the NPC to actually perform. Schedules are chosen based upon the NPC's current State and Conditions. | |
Examples might include: | |
- I'm taking cover to reload my gun | |
- I'm chasing after my enemy | |
- I'm moving to a position where I have line-of-sight to my enemy | |
NPCs will choose a new schedule for one of two reasons: | |
- They finish performing their last schedule | |
- They generate a condition that their current schedule has specified as an "Interrupt" | |
- Perform the current Task - | |
The Task is a component of a Schedule that describes a discrete action. | |
Tasks must be performed one by one for the schedule to be completed. | |
For example, the "I'm taking cover to reload my gun" schedule would be broken down into the following tasks: | |
- Find a position to take cover at | |
- Generate a path to that position | |
- Run the path | |
- Reload my gun | |
Many tasks, like the one above, take time to perform, | |
so the NPC will keep performing that task each time it thinks until the task is completed. | |
Then, it'll move onto then next task in the current schedule, or pick a new schedule if there are no tasks left. | |
If a task fails, the schedule fails. | |
--]] | |
local Orakel = require(game.ReplicatedStorage.Orakel.Main) | |
local pathLib = Orakel.LoadModule("PathLib") | |
local npcLib = Orakel.LoadModule("NpcLib") | |
local map = workspace.Map | |
local ai = {} | |
ai.PreThink = Instance.new("BindableEvent") | |
ai.PostThink = Instance.new("BindableEvent") | |
ai.States = { | |
Alive = true; | |
EnemySeen = false; | |
EnemyHeard = false; | |
Combat = false; | |
Alert = false; | |
ActBusy = false; | |
Scripted = false; | |
} | |
ai.Config = { | |
VisRange = 512; | |
HearRange = 64; | |
} | |
local schedules = {} | |
local npc, ignoreEnts, initState, initSched, masterTable, mnt_index | |
function ai.Init(Npc, IgnoreEnts, States, Schedules, InitState, InitSched) | |
npc = Npc | |
ignoreEnts = IgnoreEnts | |
if States ~= nil then | |
ai.States = States | |
end | |
if Schedules ~= nil then | |
schedules = Schedules | |
end | |
if InitState ~= nil then | |
initState = InitState | |
end | |
if InitSched ~= nil then | |
initSched = InitSched | |
end | |
masterTable, mnt_index = pathLib.CollectNodes(map.Nodes) | |
end | |
function ai.SetValue(name, value) | |
ai[name] = value | |
end | |
function ai.Think() | |
ai.PreThink:Fire() | |
local tHear, tVis = ai.Sense() | |
local conds = ai.ChooseConditions(tHear, tVis) | |
local state = ai.ChooseState(conds) | |
local tasks = ai.SelectSchedule() | |
for numTask, task in pairs(tasks) do | |
local interrupt = ai.PerformTask(task) | |
if interrupt then | |
break | |
end | |
end | |
ai.PostThink:Fire() | |
end | |
function ai.Sense() | |
local tHear = {} | |
local tVis = {} | |
for _, sound in pairs(map.Sounds:GetChildren()) do | |
local dist = (sound.Position - npc.Torso.Position).magnitude | |
if dist <= ai.Config.HearRange then | |
table.insert(tHear, sound) | |
end | |
end | |
for _, ent in pairs(map.Entities:GetChildren()) do | |
local entIsModel = (ent.Classname == "Model") | |
local lineOfSight | |
if entIsModel then | |
lineOfSight = npcLib.LineOfSight(npc.Head, ent.Primary, ai.Config.VisRange, ignoreList) | |
else | |
lineOfSight = npcLib.LineOfSight(npc.Head, ent, ai.Config.VisRange, ignoreList) | |
end | |
if lineOfSight then | |
table.insert(tVis, ent) | |
end | |
end | |
for _, mapNpc in pairs(map.NPCs:GetChildren()) do | |
local lineOfSight = npcLib.LineOfSight(npc.Head, mapNpc.Torso, ai.Config.VisRange, ignoreList) | |
if lineOfSight then | |
table.insert(tVis, mapNpc) | |
end | |
end | |
for _, plr in pairs(game.Players:GetPlayers()) do | |
local lineOfSight = npcLib.LineOfSight(npc.Head, plr.Character.Torso, ai.Config.VisRange, ignoreList) | |
if lineOfSight then | |
table.insert(tVis, plr.Character); | |
end | |
end | |
return tHear, tVis | |
end | |
function ai.ChooseConditions(tHear, tVis) | |
end | |
function ai.ChooseState(conds) | |
end | |
function ai.CreateSchedule(tasks) | |
end | |
function ai.SelectSchedule(sched) | |
end | |
function ai.PerformTask(task) | |
end | |
function ai.SetState(state, value) | |
end | |
return function() | |
return ai | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment