ID:2689991
 
(See the best response by Ter13.)
my code:




Code:
        var
is_moving = 0
base_delay = 3
move_delay = 3

tmp
next_move = 0 //the world.time value of the next allowed self movement
last_move = 0 //the world.time value of the last self moveme

Move()
for(var/mob/monster/target in view(loc))
walk_to(target, loc, 1, 5)

var/time = world.time

if(next_move>time)
return

if(disabled)
return

glide_size = ICON_SIZE / max(move_delay,TICK_LAG) * TICK_LAG
last_move = time //set the last movement time

next_move = time+move_delay
..()


if (src.current_state == walking_state_1) //if already walking, ignore
return
else if (src.current_state == running_state_1) //if already walking, ignore
return
else
if(move_delay < base_delay) //trigger to toggle to running mode
src.Change_State(running_state_1)
else
src.Change_State(walking_state_1)

spawn(3) Is_Moving()
if(!is_moving)
world << "oi"
src.Change_State(idle_state_1)

proc
Is_Moving(last_move)
if(world.time-last_move > move_delay )
is_moving = 0
else
is_moving = 1
return is_moving




Problem description:
I got back on coding DM and this project after a long time, and I may missing some minor clues.
I want to know if the mob is in constant motion or not to change its state.

Something like:
if is_moving then
current_state = walking_state
else
current_state = idle_state

In my current code, when I'm walking, the states go:
walking -> idle -> walking
This happens for every tile.

I would like to know if it is possible to only leave the walking state on the "movement keys up"

This is for movement fluency. It looks good, but I'm afraid if I try something more elaborate than what I have now, it may look glitchy.


I tried a few solutions, but this one seems to be closer.
It get stuck on idle_state.
I don't know if I coded around a limitation from DM or if I just revisited an already fully flawed piece of code from my past self.

Either way I am stuck on this problem.


This one is the visually working one, but changing states every tile.
And it looks like I coded around this seemed limitation.

Move()
for(var/mob/monster/target in view(loc))
walk_to(target, loc, 1, 5)

var/time = world.time

if(next_move>time)
return

if(disabled)
return

glide_size = ICON_SIZE / max(move_delay,TICK_LAG) * TICK_LAG
last_move = time //set the last movement time

next_move = time+move_delay
..()


if (src.current_state == walking_state_1)
return
else if (src.current_state == running_state_1)
return
else
if(move_delay < base_delay) //TODO:If run reduced the move_delay. Seems redundant
src.Change_State(running_state_1)
else
src.Change_State(walking_state_1)

spawn(max(move_delay,TICK_LAG)) src.Change_State(idle_state_1)


I'm not really sure where you are going with this.. or where to start on your question.

However, Move() doesn't return till the movement is done.

So to know if its is MOVING or "constantly moving" would be to set a bool ON during the beginning of the operation and then of course turn it off after the ..() Somewhere. (as that is doing the actually moving, so it should return when you are done)

I hope this helps.

P.S... The icon state being changed to not "standing still" Should achieve the same thing as a bool?
I believe the problem is deeper than I thought.
It always existed, but It became more obvious due to the gliding smoothing I tried to pull in my Move override.

It is breaking other stuff, such as running, I will have to revisit a lot of other things, but If I anyone could say if there is a way to get a bool for "is_moving" will be enough, thank you.
var/is_moving = FALSE

Move()
is_moving = TRUE
..()
is_moving = FALSE
Best response
This is objectively a mess.

1) walk_to calls Move(). You shouldn't be calling walk_to() inside of Move().

2) Your state-based approach assumes that Move() is only going to be called from self-moves or that Move() is only going to be called once a frame.

3) Your want for a boolean isMoving is already inferred from if(next_move>world.time)

Let's drop this and go completely back to basics, including some stuff I've learned since I released the parts of this you took from my snippets.

1) do not assume that Move() is called from self-movement. All self-movement behavior should be built into wrapper procs called by the mob's AI.

atom/movable
var
move_delay = 0
var/tmp
last_step = -1#INF
next_step = 0
proc
canStep(dir,delay)
return next_step<=world.time

Step(dir=src.dir,delay=move_delay)
if(!canStep(dir,delay))
return 0

var/old_loc = loc, old_dir = dir

glide_size = ICON_SIZE / max(move_delay,TICK_LAG) * TICK_LAG
. = step(src,dir)

if(.||src.dir!=old_dir)
Stepped(old_loc,old_dir,delay)

Stepped(atom/old_loc,old_dir,delay)
next_step = (last_step = world.time) + delay



Right, now that we've got a Step() proc settled, let's do something similar with Move()

atom/movable
proc
canMove(atom/NewLoc,Dir)
return 1

Move(atom/NewLoc,Dir)
if(!canMove(NewLoc,Dir))
return 0

var/atom/OldLoc = loc, OldDir = dir
. = ..()
if(.||dir!=OldDir)
Moved(OldLoc,OldDir,Ox,Oy)

Moved(atom/old_loc,old_dir)


Now we've got a nice little structure that will allow you to override stuff later on down the type tree.

Let's do that.

var
game_running = 1
list/tickers = list()
proc
Ticker()
set waitfor = 0
while(game_running)
for(var/client/c)
c.onTick()
for(var/datum/d in tickers)
d.onTick()
sleep(world.tick_lag)

world
New()
. = ..()
Ticker()

datum
proc
onTick()

client
proc
onTick()
mob.onTick()

ControlTick()
return 1

mob
proc
onTick()
if(!client?.ControlTick())
AITick()

AITick()



Okay, so we've got our basic ticker loop set up, but nobody can do anything. Let's build in client controls.

mob
icon_state = "walking"
move_delay = 2.5
var
run_delay = 1
walk_delay = 2.5
client
var/tmp
move_keys = 0
move_dir = 0
run_key = 0

verb
MoveKey(dir,state)
set hidden = 1, instant = 1
if(state)
move_keys |= dir
else
move_keys &= ~dir

var/vertical = move_keys&3, horizontal = move_keys&12
move_dir = (vertical!=3 ? vertical : 0) + (horizontal!=12 ? horizontal : 0)

RunKey(state)
set hidden = 1, instant = 1
run_key = state

ControlTick()
if(move_dir)
if(mob.canStep())
if(run_key)
if(mob.icon_state=="walking")
mob.icon_state = "running"
mob.move_delay = run_delay
else
if(mob.icon_state=="running")
mob.icon_state = "walking"
mob.move_delay = walk_delay

walk(src,0) //cancel repeating movement
mob.Step(move_dir)
return 1


Right, so now we've got a nice little movement loop going on for each client where they can control their mob directly. You can build in more abstraction than this and defer a lot of this behavior to mobs if your game needs it, but I doubt yours does.

You'll need to set up macros in your skin file:

W: MoveKey 1 1
W+UP: MoveKey 1 0
A: MoveKey 8 1
A+UP: MoveKey 8 0
S: MoveKey 2 1
S+UP: MoveKey 2 0
D: MoveKey 4 1
D+UP: MoveKey 4 0
Shift: RunKey 1
Shift+UP: RunKey 0


To get the movement states showing up right, you want to set them up so that both running and walking have their own idle state. You don't want to swap icon_states all the time. Instead, for each state the player can idle in, make one animated state and mark it as a movement state, and then have another one with the same name be the idle state, not marked as a movement state. BYOND will do the work animating and switching these for you here.

Next, let's work back in some of the other stuff you had going on.

mob
var
disabled = 0
snared = 0

canStep(dir,delay)
return !disabled && !snared && ..()

canMove(dir,delay)
return !snared && ..()


This bit stops movement when a particular status ailment is affecting the mob. You can have stacking disability effects by ++ and -- ing the disability variables rather than assigning them to 1 or 0 directly.


Next, let's readd that target seeking code you had, but better this time.

mob
var/tmp
mob/target
turf/aggro_loc

targeting_range = 5
target_chase_range = 10
target_chase_dist = 15
target_seek_time = 0
target_seek_delay = 10

AITick()
var/target = src.target, valid = 0, dir = 0

//attempt to locate targets if we don't have one.
if(!target)
if(target_seek_time<=world.time)
target = getValidTarget()
if(!aggro_loc)
aggro_loc = loc
valid = 1

if(target && (valid || isValidTarget(target)) && (aggro_loc && get_dist(src,aggro_loc)<=target_chase_dist))
if(get_dist(src,target)>1)
//attempt to move to target if not in attack range
if(canStep())
dir = get_step_to(target,1)
Step(dir)
else
//attack target if in range.
AIAttack(target)
else
//attempt to walk home
if(aggro_loc && canStep())
if(get_dist(src,aggro_loc)>0)
dir = get_step_to(aggro_loc,0)
if(dir)
Step(dir)
else
aggro_loc = null

mob/proc
AIAttack(atom/target)
//do nothing for now.

getValidTarget()
target_seek_time = world.time + target_seek_delay
for(var/mob/m in oview(src,targeting_range))
if(isValidTarget(m))
return m
return null

isValidTarget(mob/m)
if(m.client && get_dist(src,m)<=target_chase_range)
return 1
return 0


Now let's just enable AI on a monster:

mob/monster
New()
..()
//this helps distribute target seeking behavior a little better at world startup.
target_seek_time = rand()*target_seek_delay + world.time

global.tickers += src

//you probably wanna remove the mob from global tickers when it dies instead of this.
Del()
global.tickers -= src
..()


Honestly, if it were me, I'd do a lot of this pretty differently, but this is a bare bones way to achieve what you already had, but with less buggy structure and more options for future expansion. I would be treating AI the same as clients, and instead of having AI control themselves, I'd build an AIcontroller datum that acts exactly like a client does, but instead of reading keyboard inputs, it would read directional impulses from sensory feedback from the world.