ID:2917896
 
(See the best response by Kumorii.)
Code ENEMY-AI.dm
// File:    enemy-ai.dm
// Library: Forum_account.ActionRpgFramework
// Author: Forum_account
//
// Contents:
// This file contains the code that manages which mobs
// are active. If a mob isn't near a client it is
// deactivated to save CPU usage. This file also contains
// the ai() proc which can be used to create enemy AI.

var
list/active_mobs = list()

PixelMovement
movement()
for(var/mob/m in active_mobs)
m.check_loc()
m.movement(tick_count)

mob
var
tmp/ai_paused = 0
tmp/wander_delay = 0
tmp/activation_range = 10
tmp/active = 0
tmp/deactivation_delay = 0

// this is the number of tiles the mob will stray while wandering
tmp/wander_distance = 4

// this is the distance in pixels the mob will get from their
// target to attack them. The default, 8 pixels, is melee range.
tmp/attack_distance = 6

// this is the number of tiles around the mob will check to find
// a target and the cooldown (in ticks) between attempts to find
// a target. The target_range var is also used by the player's
// targeting - when you press tab it'll cycle through all targets
// within this range of the player.
tmp/target_range = 5
tmp/target_cooldown = 20

// enemies have a smaller default targeting range.
enemy
target_range = 3

movement(t)

// only non-client mobs can become deactivated
if(!client)

// check every 40 ticks
deactivation_delay -= 1
if(deactivation_delay <= 0)
deactivation_delay = 40

// see if there's a nearby client
var/deactivate = 1
for(var/mob/m in oview(activation_range + 1, src))
if(m.client)
deactivate = 0
break

// if no clients are nearby, deactivate
if(deactivate)
deactivate()
return

..()

action(t)

if(dead)
moved = 0

// clear the player's input so they stop
// moving even if they were holding a key
if(client)
client.clear_input()

slow_down()
return

// if the mob is alive, check if health and mana regen
// should be called.
if(health < max_health)
if(!on_cooldown("health_regen"))
cooldown("health_regen", Constants.REGEN_TICK_LENGTH)
health_regen()

if(mana < max_mana)
if(!on_cooldown("mana_regen"))
cooldown("mana_regen", Constants.REGEN_TICK_LENGTH)
mana_regen()


// only clients check for mobs to activate
if(client)

..()

// check every 40 ticks
deactivation_delay -= 1
if(deactivation_delay <= 0)
deactivation_delay = 40

for(var/mob/m in oview(activation_range, src))
m.activate()

// for non-client mobs, call their ai() proc
else
if(moved)
moved = 0
else
slow_down()

if(ai_paused)
return

if(path || destination)
follow_path()

if(!ai_paused)
ai()

proc
// by default these do nothing, they're just called
// periodically and you can implement regeneration
// however you'd like.
health_regen()
mana_regen()

// by default, the mob's AI makes them wander around, look
// for targets, and attack their targets.
ai()
// if the enemy has a target, move towards it
if(target2)
// world << "Mob position: [loc]"
// world << "Target position: [target.loc]"
// world << "Distance to target: [bounds_dist(src, target)]"
if(bounds_dist(src, target2) > attack_distance)
move_towards(target2)
else
stop()

// try using each attack
if(abilities)
for(var/Ability/ability in abilities)
use_ability(ability)

// otherwise we wander around and look for a target
else
if(wander_distance)
wander(wander_distance)

// we use cooldowns to limit how often we look for targets.
if(!on_cooldown("find-target"))

// look for a target
target2 = find_target(target_range)

// if we didn't find a target, wait before checking again
if(!target2)
cooldown("find-target", target_cooldown)

ai_pause()
ai_paused = 1

ai_play()
ai_paused = 0

// makes the mob move to a random turf located within a
// specified range of the mob's initial position.
wander(range = 3)
if(!loc) return
if(!x) return
if(!initial(x)) return

if(!path)
var/turf/t = loc

while(!t || t.density || t == loc)
t = locate(initial(x) + rand(-range, range), initial(y) + rand(-range, range), z)

if(t)
move_to(t)

activate()
if(active) return

active = 1
active_mobs += src
deactivation_delay = rand(60, 90)

for(var/Condition/c in conditions)
c.activate()

deactivate()
if(!active) return

active = 0
active_mobs -= src

if(client)
for(var/Condition/c in conditions)
c.deactivate()

// enemy AI helper procs
find_target(range = 4)
for(var/mob/m in oview(range, src))
if(can_attack(m))
return m





Code PATHING.dm
// File:    pathing.dm
// Library: Forum_account.PixelMovement
// Author: Forum_account
//
// Contents:
// This file contains pathfinding features. It defines the
// mob.move_to and mob.move_towards procs which are similar
// to DM's built-in walk_to and walk_towards procs.

// The /Path datum is used to contain all A* pathfinding code.
// You never need to access this directly, mob.move_to handles
// all the details.
//
// The implementation is almost directly copied from:
// http://en.wikipedia.org/wiki/A*_search_algorithm
Path
var
turf/destination
mob/mover
list/tiles

list/closed
list/fringe
list/parent
turf/current

list/f_score
list/g_score
list/h_score

limit = 0

New(mob/m, turf/t)
mover = m
destination = t

compute()

proc
// The add proc is called to add a node to the fringe. If the node already
// exists in the fringe, it's g() and h() values are updated (if appropriate).
add(turf/t)
if(!t) return
if(t.density) return
if(t in closed) return 0

var/tentative_g_score = g_score[current] + distance(current, t)
var/tentative_is_better = 0

if(!(t in fringe))
fringe += t
tentative_is_better = 1
else if(tentative_g_score < g_score[t])
tentative_is_better = 1

if(tentative_is_better)
parent[t] = current

g_score[t] = tentative_g_score
h_score[t] = heuristic(t)
f_score[t] = g_score[t] + h_score[t]

return 1

// this proc controls the distance metric used by the search algorithm.
distance(turf/a, turf/b)
return abs(a.x - b.x) + abs(a.y - b.y)

// the heuristic is simply the distance from the turf to the destination.
heuristic(turf/t)
return distance(t, destination)

compute()

closed = list()
fringe = list(mover.loc)
parent = list()

f_score = list()
g_score = list()
h_score = list()

g_score[mover.loc] = 0
h_score[mover.loc] = heuristic(mover.loc)
f_score[mover.loc] = h_score[mover.loc]

parent[mover.loc] = null

var/found_path = 0

while(fringe.len)

// if there's a limit to how many turfs you'll check
if(limit)
// and if you've reached that limit, stop
if(closed.len >= limit)
break

// find the node with the lowest f-score
current = fringe[1]

for(var/turf/t in fringe)
if(f_score[t] < f_score[current])
current = t

// If this node is the destination, we're done.
if(current == destination)
found_path = 1
break

fringe -= current
closed += current

var/move_right = add(locate(current.x + 1, current.y, current.z))
var/move_up = add(locate(current.x, current.y + 1, current.z))
var/move_left = add(locate(current.x - 1, current.y, current.z))
var/move_down = add(locate(current.x, current.y - 1, current.z))

if(move_right)
if(move_up)
add(locate(current.x + 1, current.y + 1, current.z))
if(move_down)
add(locate(current.x + 1, current.y - 1, current.z))

if(move_left)
if(move_up)
add(locate(current.x - 1, current.y + 1, current.z))
if(move_down)
add(locate(current.x - 1, current.y - 1, current.z))

// At this point we're outside of the loop and we've
// either found a path or exhausted all options.

if(!found_path)
del src

// at this point we know a path exists, we just have to identify it.
// we use the "parent" list to trace the path.
var/turf/t = destination
tiles = list()

while(t)
tiles.Insert(1, t)
t = parent[t]

mob
var
Path/path
turf/destination
turf/next_step

__stuck_counter = 0

proc
// The follow_path proc is called from the mob's default movement proc if
// either path or destination is set to a non-null value (in other words,
// follow_path is called if move_to or move_towards was called). Because
// this proc is called from movement, all we need to do is determine what
// calls to move, jump, or climb (not implemented yet) should be made.
follow_path()

// If the mob is following a planned path...
if(path)

if(loc == path.destination)
moved_to(path.destination)
stop()
return 1

// check to see if the mob is "stuck", the stuck counter
// is reset to zero when the mob advances a tile. If the
// mob spends 50 ticks at the same tile, they're "stuck".
__stuck_counter += 1

if(__stuck_counter > 50)
__stuck_counter = 0
path = new(src, path.destination)

if(!path)
return 0

next_step = null
while(next_step == null)
if(!path.tiles.len)
next_step = path.destination
break

next_step = path.tiles[1]

// if we've reached the next tile, remove it from
// the path and reset the stuck counter
if(next_step in locs)
path.tiles.Cut(1,2)
next_step = null
__stuck_counter = 0

// now that the library supports 8-directional moves, we can
// do it this way:
var/move_east = (px < next_step.px - vel_x)
var/move_west = move_east ? 0 : (px + pwidth + vel_x > next_step.px + next_step.pwidth)
var/move_north = (py < next_step.py - vel_y)
var/move_south = move_north ? 0 : (py + pheight + vel_y > next_step.py + next_step.pheight)

if(move_east)
if(move_north)
move(NORTHEAST)
dir = NORTHEAST
else if(move_south)
move(SOUTHEAST)
dir = SOUTHEAST
else
move(EAST)
dir = EAST

else if(move_west)
if(move_north)
move(NORTHWEST)
dir = NORTHWEST
else if(move_south)
move(SOUTHWEST)
dir = SOUTHWEST
else
move(WEST)
dir = WEST

else
if(move_north)
move(NORTH)
dir = NORTH
else if(move_south)
move(SOUTH)
dir = SOUTH
else
moved_to(path.destination)
stop()

// if the mob is moving towards a destination...
else if(destination)

var/bounds_dist = bounds_dist(src, destination)
if(bounds_dist < -8)
moved_towards(destination)
stop()
return 1
else if(bounds_dist < 1 && can_bump(destination))
moved_towards(destination)
stop()
return 1

// I made the same changes to these if statements as the ones
// in the case for following a path.
var/slow_down = 0
if(px < destination.px)
move(EAST)
dir = EAST
else if(px +pwidth > destination.px + destination.pwidth)
move(WEST)
dir = WEST
else
slow_down += 1

if(py < destination.py)
move(NORTH)
dir = NORTH
else if(py + pheight > destination.py + destination.pheight)
move(SOUTH)
dir = SOUTH
else
slow_down += 1

if(slow_down == 2)
slow_down()

else
return 0

proc
// calling the stop proc will halt any movement that was
// triggered by a call to move_to or move_towards.
stop()
destination = null
path = null

// This is the pixel movement equivalent of DM's built-in walk_towards
// proc. The behavior takes some obstacles into account (it'll try to
// jump over obstacles) but it doesn't plan a path. It's CPU usage is
// lower than move_to but the behavior may not be sufficiently smart.
move_towards(atom/a)

stop()

// calling move_towards(null) will stop the current movement but
// not trigger a new one, so it's just like calling stop().
if(!a) return 0

destination = a

return 1

moved_to(turf/t)
moved_towards(atom/a)

// move_to is the pixel movement equivalent of DM's built-in walk_to proc.
// It uses the A* algorithm to plan a path to the destination and the
// follow_path proc handles the details of following the path.
move_to(turf/t)

stop()

if(istype(t, /atom/movable))
t = t.loc

// calling move_to(null) will stop the current movement but
// not trigger a new one, so it's just like calling stop().
if(!t) return 0

// Because we're creating a new path we can reset this counter.
__stuck_counter = 0

path = new(src, t)

if(path)
next_step = path.tiles[1]
return 1
else
stop()
return 0





Hey guys. I'm using the action-rpg-framework, which includes Forum_account's pixel movement and has a bug that I can't seem to resolve. Apparently, the Slime monster rotates while walking towards you. I've been trying to understand this, but if someone has encountered it before, I would appreciate the help. I noticed that it rotates if it's coming from the east or west towards you. This doesn't happen when the Slime is wandering around looking for someone to attack. It only occurs when it finds someone. I've tried tinkering with the enemy-ai.dm and even within the Pixel Movement's pathing.dm, but I haven't had any positive results. Could anyone help me?

Best response
Forum_Account's libraries are pretty outdated, especially his pixel movement library. They aren't really worth using and will teach a lot of poor practices for modern development with BYOND. Not giving flak to FA, he was awesome at what he did back in the day, but the engine has evolved a ton and now includes native pixel movement and new approaches to handle a lot of the stuff his libraries manually did. They're needlessly overcomplicated these days.

To learn modern DM movement foo, I'd suggest checking out MoveLib by Ter13.

He also did a really insightful Snippet Sunday that goes into movement a bit here. Not explicitly about pixel movement, but a lot of what he covers is applicable to pixel movement as well.
In response to Kumorii
Kumorii wrote:
Forum_Account's libraries are pretty outdated, especially his pixel movement library. They aren't really worth using and will teach a lot of poor practices for modern development with BYOND. Not giving flak to FA, he was awesome at what he did back in the day, but the engine has evolved a ton and now includes native pixel movement and new approaches to handle a lot of the stuff his libraries manually did. They're needlessly overcomplicated these days.

To learn modern DM movement foo, I'd suggest checking out MoveLib by Ter13.

He also did a really insightful Snippet Sunday that goes into movement a bit here. Not explicitly about pixel movement, but a lot of what he covers is applicable to pixel movement as well.

Forum Account's pixel movement library actually uses the native pixel movement system BTW. Also, his implementation of the movement andcollision itself is pretty good imo. However, I do not believe his movement loop works at scale.

Out of his family of libraries, this particular one has age the best.

Now there are dependencies that he uses -- like his key tracking library -- that are majorly outdated indeed.
In response to Kumorii
Unfortunately I already have a more advanced project using the action-rpg-framework. I believe it's a bit late to start from scratch again :/ I'm working on the game as a hobby, and the movement is working fine, but I only have the doubt I mentioned about the AI movement when it encounters a target. I'm still trying to figure out what the exact cause of this is
In response to Enzo0123
Enzo0123 wrote:
Unfortunately I already have a more advanced project using the action-rpg-framework. I believe it's a bit late to start from scratch again :/ I'm working on the game as a hobby, and the movement is working fine, but I only have the doubt I mentioned about the AI movement when it encounters a target. I'm still trying to figure out what the exact cause of this is

I'll try to look at it later. But note that you've posted a crap ton of code. Is all of this code relevant? If not, I would say just post what the slime's movement call is.
In response to Meme01
I'm sorry for sending the whole code hahah. I just wanted it to be clear for you guys :)
I worked around the issue by changing "move_towards(target2)" to "move_to(target2)" in enemy-ai.dm (action-rpg-framework). So, the slime doesn't spin around when coming to attack me, but it seems to behave in a less intelligent way.

I believe there's something in pathing.dm (pixel-movement) that isn't well elaborated. I'll show only the code I changed in enemy-ai.dm (action-rpg-framework) and where I believe the error lies in pathing.dm.


enemy-ai.dm



    proc
health_regen()
mana_regen()

// by default, the mob's AI makes them wander around, look
// for targets, and attack their targets.
ai()
// if the enemy has a target, move towards it
if(target2)
if(bounds_dist(src, target2) > attack_distance)
move_to(target2)
// move_towards(target2)
else
stop()





Now the code pathing.dm with the part I believe has the error.
It's going to be quite a bit of code, but this is the area I've modified to work around the error



pathing.dm




mob
var
Path/path
turf/destination
turf/next_step

__stuck_counter = 0

proc
// The follow_path proc is called from the mob's default movement proc if
// either path or destination is set to a non-null value (in other words,
// follow_path is called if move_to or move_towards was called). Because
// this proc is called from movement, all we need to do is determine what
// calls to move, jump, or climb (not implemented yet) should be made.
follow_path()

// If the mob is following a planned path...
if(path)

if(loc == path.destination)
moved_to(path.destination)
stop()
return 1

// check to see if the mob is "stuck", the stuck counter
// is reset to zero when the mob advances a tile. If the
// mob spends 50 ticks at the same tile, they're "stuck".
__stuck_counter += 1

if(__stuck_counter > 50)
__stuck_counter = 0
path = new(src, path.destination)

if(!path)
return 0

next_step = null
while(next_step == null)
if(!path.tiles.len)
next_step = path.destination
break

next_step = path.tiles[1]

// if we've reached the next tile, remove it from
// the path and reset the stuck counter
if(next_step in locs)
path.tiles.Cut(1,2)
next_step = null
__stuck_counter = 0

// now that the library supports 8-directional moves, we can
// do it this way:
var/move_east = (px < next_step.px - vel_x)
var/move_west = move_east ? 0 : (px + pwidth + vel_x > next_step.px + next_step.pwidth)
var/move_north = (py < next_step.py - vel_y)
var/move_south = move_north ? 0 : (py + pheight + vel_y > next_step.py + next_step.pheight)

if(move_east)
if(move_north)
move(NORTHEAST)
dir = NORTHEAST
else if(move_south)
move(SOUTHEAST)
dir = SOUTHEAST
else
move(EAST)
dir = EAST

else if(move_west)
if(move_north)
move(NORTHWEST)
dir = NORTHWEST
else if(move_south)
move(SOUTHWEST)
dir = SOUTHWEST
else
move(WEST)
dir = WEST

else
if(move_north)
move(NORTH)
dir = NORTH
else if(move_south)
move(SOUTH)
dir = SOUTH
else
moved_to(path.destination)
stop()

// if the mob is moving towards a destination...
else if(destination)

var/bounds_dist = bounds_dist(src, destination)
if(bounds_dist < -8)
moved_towards(destination)
stop()
return 1
else if(bounds_dist < 1 && can_bump(destination))
moved_towards(destination)
stop()
return 1

// I made the same changes to these if statements as the ones
// in the case for following a path.
var/slow_down = 0
if(px < destination.px)
move(EAST)
dir = EAST
else if(px +pwidth > destination.px + destination.pwidth)
move(WEST)
dir = WEST
else
slow_down += 1

if(py < destination.py)
move(NORTH)
dir = NORTH
else if(py + pheight > destination.py + destination.pheight)
move(SOUTH)
dir = SOUTH
else
slow_down += 1

if(slow_down == 2)
slow_down()

else
return 0

proc
// calling the stop proc will halt any movement that was
// triggered by a call to move_to or move_towards.
stop()
destination = null
path = null

// This is the pixel movement equivalent of DM's built-in walk_towards
// proc. The behavior takes some obstacles into account (it'll try to
// jump over obstacles) but it doesn't plan a path. It's CPU usage is
// lower than move_to but the behavior may not be sufficiently smart.
move_towards(atom/a)

stop()

// calling move_towards(null) will stop the current movement but
// not trigger a new one, so it's just like calling stop().
if(!a) return 0

destination = a

return 1

moved_to(turf/t)
moved_towards(atom/a)

// move_to is the pixel movement equivalent of DM's built-in walk_to proc.
// It uses the A* algorithm to plan a path to the destination and the
// follow_path proc handles the details of following the path.
move_to(turf/t)

stop()

if(istype(t, /atom/movable))
t = t.loc

// calling move_to(null) will stop the current movement but
// not trigger a new one, so it's just like calling stop().
if(!t) return 0

// Because we're creating a new path we can reset this counter.
__stuck_counter = 0

path = new(src, t)

if(path)
next_step = path.tiles[1]
return 1
else
stop()
return 0






Thank you for the help! :D