ID:2588173
 
(See the best response by Kaiochao.)
Code:
mob/proc/Hunt()
var/mob/PC/P
while(src)
if(P in oview(10))
walk_to(src,P,1,4)
if(P in oview(1))
step_towards(src,P)
else
step_rand(src)
break
else
for(P in view(src))
break
sleep(5)
spawn(2)
Hunt()

mob/proc/Wander()
var/mob/PC/P
while(src)
if(P in oview(10))
if(P.name in src.killlist)
walk_to(src,P,1,4)
if(P in oview(1))
step_towards(src,P)
else
sleep(15)
step_rand(src)
break
else
for(P in view(src))
break
sleep(5)
spawn(2)
Wander()


Problem description:
I am currently working on the (basic) monster AI for my game. It works as intended, I am here hoping maybe someone could take a look and tell me if there's a better way to do this?

The idea is I set the aggression of the monster at New(), Wander has them just wander around (until attacked) and Hunt has them go directly to a player if they are within the proper view.
Generally, you don't want your NPCs doing anything unless there's a player nearby to appreciate it.

You can actually leverage the bounds variable and the Cross/Crossed() procs to activate NPCs.

As for wandering part, you'd probably want something that determines what region NPCs belong to, then start their wandering around when a player enters that region, and cease it when there are no players left in the region.

This way you don't have a bunch of NPCs eating up CPU cycles unless there's something nearby to appreciate it.
What Nadrew said is sound advice. Let your AI sleep when no player is around and wake it when they are.

The Wander() proc you have is broken because P is null. If you want to find a player in range, you should use locate(). However it's probably easier to confine your NPCs to an area and simply wake/sleep their AI when players are in the same area or adjacent.
Yeah, I didn't even know about the whole Bounds and Cross(ed) procs. I am having a hard time understanding it from the help file though. I was looking for maybe a lib on the site to make it easier to understand, but I haven't seen anything so far.

I appreciate you guys bringing it to my attention and I am going to make an effort to understand and use it.
Have an example of a Crossed-based player detector triggering an NPC:
Source zip
Here's a rough idea of what you'd want to do.

mob
proc/AI()
var/running_ai // keep track of whether the AI is running so it doesn't get double-called

mob/monster1/AI()
// prevent this from being double-called
if(running_ai++) return
// read the area's awake var
while(isturf(loc) && length(loc.loc:players))
...
sleep(sleep_time)
running_ai = 0

Alternatively you could do something different like a global event loop that keeps track of all awake NPCs/enemies and calls a version of AI() that doesn't loop.

(Normally I don't advise new users to use the : operator because it skips any compile-time safety checks that . will use, but I think in this instance it's reasonable. Like any tool it can be misused.)

Now for each area, you might give it a list of neighboring areas, which it could look up by type. Here's something that will automate itself:

area
var/awake
var/list/players // which players are here
var/list/neighbors

proc/GetNeighbors()
if(!neighbors)
neighbors = list()
var/area/A
for(var/turf/T in src)
// we could make this a loop but let's unroll it for performance
var/turf/N = get_step(T, NORTH)
if((A=N.loc) != src) neighbors[A] = 1
N = get_step(T, SOUTH)
if((A=N.loc) != src) neighbors[A] = 1
N = get_step(T, EAST)
if((A=N.loc) != src) neighbors[A] = 1
N = get_step(T, WEST)
if((A=N.loc) != src) neighbors[A] = 1
return neighbors

proc/CheckAwake()
if(awake)
/*
The reason we spawn here is that players exit an area before they enter
the new one, so we don't want to put the area to sleep until we're sure
its neighbor isn't awake. The spawn lets us do this after the new area
is ready.
*/

spawn()
if(length(players)) return
for(var/area/A in GetNeighbors())
if(length(A.players)) return
awake = 0
else
// more work has to be done here to wake up the NPCs
if(length(players)) WakeUp()
for(var/area/A in GetNeighbors())
if(length(A.players))
WakeUp()
return

proc/WakeUp()
awake = 1
/*
TODO: Looping through area contents is way less efficient than having a list
of mobs in the area to wake up. Eventually you'll want to improve on this
but it works well until your game gets big.
*/

for(var/mob/M in src)
spawn() M.AI()

That GetNeighbors() code is not terribly efficient but it only needs to run once per area.

Now, when your mob moves around, you'll naturally enter and exit areas.

area
Exit(mob/M)
if(!ismob(M) || M.client) return 1
return 0 // NPCs and monsters can't leave the area

Entered(mob/M)
if(!ismob(M) || !M.client) return
if(!players) players = new // create the list
players[M] = 1
CheckAwake()
// you can do other stuff here too, like change the music the player hears

Exited(mob/M)
if(!ismob(M) || !M.client) return
if(!players) return
players -= M
CheckAwake()

Now there's only one other thing to look out for here, and that's if you were to change a player's loc directly without them moving around or calling Move(). (There are reasons you might not want to call Move(), like if you want them to step into a doorway and go inside a house. Usually this kind of thing is done in the doorway's Entered() proc, but if you call Move() it'll call Entered() for the opposite doorway and things will just bounce back and forth forever. Also Move() would allow something standing in the doorway to block you.) So to deal with this, if you ever need to change loc manually you should have a proc do it for you.

atom/movable/proc/Teleport(oldloc, newloc)
var/area/oldarea
var/area/newarea
// use these empty for loops to find the area
for(oldarea=oldloc, oldarea && !isarea(oldarea), oldarea = oldarea.loc);
for(newarea=newloc, newarea && !isarea(newarea), newarea = newarea.loc);
// do the teleport
loc = newloc
// call Exited() and Entered()
if(oldarea != newarea)
oldarea?.Exited(src, newarea)
newarea?.Entered(src, oldarea)
In response to Kaiochao
Kaiochao wrote:
Have an example of a Crossed-based player detector triggering an NPC:
Source zip

Thanks, I just got around to taking a look at this and I think I get the idea.

I do want to ask, is there a reason why you are setting the parent time like that instead of just doing like obj/Monster? Is there a different effects based on how you are setting the parent type? Should I not be doing it how I have been?
Honestly, don't screw with parent_type. I find it's easily and often abused. Just use normal type paths like you were doing.

However, I can't see a legit reason you'd want monsters to be objs instead of mobs.
In response to Lummox JR
Best response
Objs are the simplest type that satisfies the requirement of the demo, which is to show how to make something move in response to a trigger. There's no reason for it to be a mob, in the demo, as it is.

Mobs have the capability to be controlled by clients in the built-in way. Not particularly useful, as smooth games these days do away with built-in controls; input handling as well as camera positioning.

Mobs have special visual capabilities, which may be useful for monsters to have. Not used in the demo, but yeah, a legitimate reason.

In a more developed project, mobs may contain all the code that deals with combat. However, making monsters mobs is not the only (or the best) way to give monsters combat capabilities. The mob-specific combat code can be extracted (refactored) into a new type, a component that can be shared by combatant mobs as well as combatant objs. Easier done in a more powerful programming language, but still doable, and the code is arguably better by the end if done right. Can always go into further detail, but the concept is language-agnostic, so it's out there if you know where to look.

To change the inherited type of monsters in my demo from what /obj to /mob, you only need to change the "parent_type = /obj" line to "parent_type = /mob". If the inheritance is part of the path, then you'd need to change it everywhere the type is used: other definitions of the type, variables and proc parameters declared with that type, and in the map editor. The important thing is that it's a monster. That it's an obj or a mob, or what subtype of obj or mob it is, is free to change.