ID:1565616
 
How do some of you handle keeping an object's icon_state up-to-date? I find myself having to use if more frequently than I would like to.
Would you mind elaborating it a bit more, what do you mean by that?
In response to Taitz
For example:

        src.icon_state = animation

//...

spawn(3) //dash recovery.
if(src)
src.dashing = 0
if(src.icon_state == animation)
src.icon_state = null


Here, I am making sure the mob's state is still in dash mode (I wouldn't want to mess up another animation, like a stun for example).

You don't need the if(src). If the atom has been deleted, the spawned code won't execute in the first place.
In response to Topkasa
Topkasa wrote:
You don't need the if(src). If the atom has been deleted, the spawned code won't execute in the first place.

Unless the function has been cut off from the src scope and forced into the global scope by:

src = null


Of course, that's not the case in FKI's example there, as he's still accessing src.
In response to Topkasa
Good to know, but that's beside the point.
I wouldn't bother worrying about the icon_state, and just consider it an artifact of behavior.

I like to consolidate behavior into datums, and restrict mobs to a single action at a time. This way, I can store as much or as little information about an action as I want.

mob
actor
var/tmp
action_start
action/action
proc
Act(action/new_action,list/params=null,force=0)
if(!force && !action.CanAct(src,new_action,params) && !new_action.CanForce(src,params))
return 0
action.Yield(src,new_action,params)
new_action.Begin(src,params)
return 1


action
var
id
proc
//whether this action permits yielding to new action
CanAct(mob/actor/actor,action/new_action,list/params=null)
return 1

//allows this action to contradict another's CanAct()
CanForce(mob/actor/actor,list/params=null)
return 0

//called when this action is forcibly ended
Yield(mob/actor/actor,action/new_action,list/params=null)

Begin(mob/actor/actor,list/params=null)
actor.action = src
actor.action_start = world.time
New(id=null)
if(id)
src.id = id



A quick, somewhat complicated implementation to demonstrate their use. In this example, we're allowing players to Move once per frame only. We have also implemented a few interesting situational effects. For starters, if a player is stunned, this counts as an action, because a player can't do anything else while stunned.

Side note: I wouldn't recommend all of your status conditions being actions, because things like poison, polymorph, confusion, to name a few might allow the player to continue acting. Those types of things would need to be implemented separately from actions. In this setup, status ailments like Sleep would actually work really well, because any time a player is attacked, you can apply a Stunned action. Since actions replace the old action on the record, the player would wake up out of Sleep automatically if attacked in our setup.

We have given the player two attacks. One is a regular attack, which hits everything within a bounding box the size of the player in the direction they are facing, and knocks them back 2 steps, and stuns them for 2 frames.

If the player uses an attack on the same frame that they have moved, they will attempt to charge in the direction they are moving. This boosts them forward by 10 steps, and hits anything along that path. The impact with the player also imparts a knockback and stun of 10 steps plus however many steps the player has left in their charge, as well as stuns them for 10 ticks plus however many steps the player has left in their charge. Should the player hit anything that doesn't move out of the way over the duration of this attack, the player will be stunned himself for the remaining number of ticks the attack would have occurred during.

The correct use of the action states allows you to take precise control of animations, visual changes, and even game behavior. Used correctly, you will have a very solid-feeling combat system at your disposal, and you won't ever have to worry about weird bugs cropping up due to attacks being canceled early, functions happening twice in rapid succession, etc.

#define TICK_LAG 1
#define TILE_WIDTH 32
#define TILE_HEIGHT 32

#define KNOCKBACK_STEP 16 //if you aren't using pixel movement, set these to 0
#define CHARGE_STEP 16

client
verb
//bind this to a macro in the interface macros list, or call using my controller library
attack()
set hidden = 1
set instant = 1
//normally I recommend against the ":" operator being used, but in this case, I'll make an exception for the sake of speed.
src.mob:Act(actions["attack"])

var
list/actions = list("idle"=new/action/idle(),"walk"=new/action/walk(),"attack"=new/action/attack(),"charge"=new/action/charge(),"stunned"=new/action/stunned())

mob
actor
var
move_delay = TICK_LAG
var/tmp
control = null
proc
TakeDamage(source,damage,knockback_dir=SOUTH,knockback_steps=0,stun_duration=0)
set waitfor = 0 //allow the attack's loop to proceed as normal
//execute the action now, but don't wait for it to finish
if(stun_duration)
spawn(-1)
src.Act(actions["stunned"],list("duration"=stun_duration),1)
var/action/src_act = src.action
var/src_time = src.action_start
var/o_control
//take a step in the knockback direction every
for(var/count=0;count<knockback_steps;count++)
if(src.action==src_act && src.action_start==src_time)
o_control = src.control
src.control = src_act
. = step(src,knockback_dir,KNOCKBACK_STEP)
src.control = o_control
//if movement fails, stop and let the stun take over
if(!.)
return
sleep(TICK_LAG)
else
return

Idle()
//forcibly idle
Act(actions["idle"],null,1)

Attack()
//track the current action
var/action/src_act = src.action
var/src_time = src.action_start

//begin animating the attack
src.icon_state = "attack"

//set up the attack bounds
var/off_x = 0
var/off_y = 0
if(NORTH&src.dir)
off_y += 1
if(SOUTH&src.dir)
off_y -= 1
if(EAST&src.dir)
off_x += 1
if(WEST&src.dir)
off_x -= 1

//prepare to store the targets
var/list/targets = bounds(src,off_x*src.bound_width,off_y*src.bound_height)
for(var/mob/actor/a in targets)
if(a!=src)
a.TakeDamage(src,10,src.dir,2,2)
sleep(TICK_LAG * 3)
if(src.action==src_act && src.action_start==src_time)
src.Idle()

Charge()
var/action/src_act = src.action
var/src_time = src.action_start

//begin animating the charge
src.icon_state = "charge"

var/list/targets
//we don't want to hit anything twice
var/list/immune = list(src)

//set the attack bounds
var/off_x = 0
var/off_y = 0

if(NORTH&src.dir)
off_y += step_size
if(SOUTH&src.dir)
off_y -= step_size
if(EAST&src.dir)
off_x += step_size
if(WEST&src.dir)
off_x -= step_size

var/duration = 10
var/o_control

//every frame, look for targets within range, deal damage if possible, then try to move forward.
//if movement fails, self-stun for the remaining duration.
while(duration)
//ensure the action is relevant
if(src.action==src_act && src.action_start==src_time)
//loop through all targets in range
targets = bounds(src,off_x,off_y)
targets.Remove(immune)
for(var/mob/actor/a in targets)
a.TakeDamage(src,10,src.dir,duration,16)
immune.Add(a)

//store the old controlling datum, set the control to the charge action, step, then reset the controlling datum.
o_control = src.control
src.control = src_act
. = step(src,src.dir,CHARGE_STEP)
src.control = o_control

//if the step succeds, decrement duration, otherwise, self-stun and end action now.
if(.)
duration--
else
src.Act(actions["stunned"],list("duration"=duration * TICK_LAG),1)
return 0
sleep(TICK_LAG)
else
return 0

if(src.action==src_act && src.action_start==src_time)
src.Idle()
return 1

Stunned(duration)
var/action/src_act = src.action
var/src_time = src.action_start
//begin animating
src.icon_state = "stunned"
src.overlays += /obj/overlays/stun_effect

//check every frame, so we can remove the effects if the stun effect ends early
while(duration)
if(src.action==src_act && src.action_start==src_time)
duration -= TICK_LAG
sleep(TICK_LAG)
else
break

//clean up
src.overlays -= /obj/overlays/stun_effect
if(src.action==src_act && src.action_start==src_time)
src.Idle()

Move()
if(control==src)
if(src.Act(actions["walk"]))
. = ..()
else
return 0
else
. = ..()
New(loc)
control = src
src.action_start = world.time
src.action = actions["idle"]
..(loc)

action
idle
Begin(mob/actor/actor)
. = ..()
var/start = actor.action_start
spawn(TICK_LAG)
if(actor.action==src && actor.action_start==start)
actor.icon_state = "idle"
walk
CanAct(mob/actor/actor,action/new_action)
return 0

//walk shouldn't block processing in this case, as Move requires an instant determination of whether we can move
Begin(mob/actor/actor)
set waitfor = 0
. = ..()
//we need to make sure that not only the action is equal to src, but the time it was set is the same.
var/start = actor.action_start
if(actor.icon_state!="walk")
actor.icon_state = "walk"
sleep(actor.move_delay)
//always check after an execution delay that this action is still relevant before doing anything
if(actor.action==src && actor.action_start==start)
actor.Idle()

attack
id = "attack"

CanAct(mob/actor/actor,action/new_action)
//this action cannot be interrupted normally
return 0

CanForce(mob/actor/actor)
//if we've just moved in the same frame, attempt to charge instead of attacking
if(actor.action==actions["walk"])
actor.Act(actions["charge"])
return 0

Begin(mob/actor/actor)
..()
//let the actor decide its own behavior/animation
actor.Attack()

charge
id = "charge"

CanAct(mob/actor/actor,action/new_action)
//this action cannot be interrupted normally
return 0

CanForce(mob/actor/actor)
if(actor.action==actions["walk"])
return 1
return 0

Begin(mob/actor/actor)
. = ..()
//again, allow the mob to define its own behavior.
actor.Charge()

stunned
id = "stunned"

CanAct(mob/actor/actor,action/new_action)
return 0

CanForce(mob/actor/actor)
return 1

Begin(mob/actor/actor,list/params)
. = ..()
//again, allow the mob to define its own behavior.
actor.Stunned(params["duration"])


Does that answer your question a bit better, FKI? (I took the liberty of writing this to work with pixel movement AND tiled movement, since I know you've taken the journey toward learning how to pixel movement. To tie this in to my movement loop/edge sliding system, you'll have to do a bit of tweaking. If you have any trouble with that, give it a shot and let me know what you are having trouble with via PM.)
Further side-notes:

If the player's connection dies while an action is not idle, don't let the player's mob be deleted. Action has been set to tmp. If you have an action that should persist through logging out, I suggest to you that you are doing it wrong. If you ever reach null reference errors in an action, the action is likely programmed correctly. The error is originating from an unsafe manual object deletion you did elsewhere in your code.

Secondly, there's a side-effect of the control variable. Since control defaults to src, the object will never be garbage collected until you break the self-reference. If you want to allow the object to be swept up by the garbage collector, you need to, at some point, set control back to null. This won't change manual deletion, but I strongly urge developers to avoid explicitly calling del on any objects at all. It's lazy, can cause massive CPU issues, and if you don't take care, you can wind up with memory leaks. This is why paying attention to your memory management from the get-go is all-important.
Nice read.

I don't use del at all except for deleting mobs after you disconnect (I couldn't get this to work with garbage collection, the mob was still being used when the player reconnected).
In response to Ter13
Slightly off-topic, do you have any good reads on garbage collection design patterns?
Patterns for what, like ... to implement in applications yourself, or ... ?
More for *remembering* to null your references. I mean, is it something that you just need to make sure you're doing it yourself, or is there particular practices that can be followed to ensure proper cleanup without thinking about where it has been set?

An example of which would be using scope to allow all references to be erased when you're out of scope.
Well, it's essentially just a logical matter of when you're not using it, that's it. All registers / adds have a corresponding unregister / remove. Once hooked around Login/Logout, you're pretty much golden.

This isn't a particularly mind-boggling task, for the most part.
I suppose I should tack on, the reason I'm saying this is that the more complex you make a task out to be to beginners, the more likely it'll be over-thought, considered too difficult for them to do etc, and so, the less likely it happens.

It's common sense. Want stuff to tidy up automatically? Make sure you've not still got it in a variable or list somewhere. Wanna make sure it's not still in a variable or list somewhere? Ask the question "what happens to this when I'm done with the attack, or when the user logs out?" and write code accordingly.
In response to Stephen001
Stephen001 wrote:
I suppose I should tack on, the reason I'm saying this is that the more complex you make a task out to be to beginners, the more likely it'll be over-thought, considered too difficult for them to do etc, and so, the less likely it happens.

It's common sense. Want stuff to tidy up automatically? Make sure you've not still got it in a variable or list somewhere. Wanna make sure it's not still in a variable or list somewhere? Ask the question "what happens to this when I'm done with the attack, or when the user logs out?" and write code accordingly.

I fully agree with Stephen on this. Though, there are some corner cases for videogames in particular where it's necessary to avoid direct referencing of objects in order to avoid problems. In some areas, it's better to abstract a reference to an object and sacrifice speed for the sake of a look-up, or to store a user vector and inform them of an object no longer being in a useful context. However, that's really not a topic I'm not interested in teaching, as I know it'll be used in areas where it's not useful should I push the example out to this community.
I know what you mean. I am very OCD about the code I write, often restarting as a result.
Doing it right the first time is easier than doing it wrong and refactoring later.

It's funny, that six months ago, most of the complaints about my advice is that it would "cause lag", or that BYOND "couldn't handle" what I claimed it could. Then I proved it could, and everybody's complaints are that my examples are needlessly complicated.

Haters gonna hate.
You're not wrong, Walter.

I think a compromise between your approach and mine would definitely be effective. After all, Epic: Legend saw a very promising initial release, but all the problems of fast development stacked up on you immediately thereafter. Paying better attention to design considerations could have helped you, and not over-complicating things could well help me.
Ter13 wrote:
You're not wrong, Walter.

In response to FKI
FKI wrote:
I know what you mean. I am very OCD about the code I write, often restarting as a result.

Ugh, I know exactly what you mean. I was told to not fix what isn't broken once, but I find myself rewriting and repairing old programming ALL the time. Several projects have been restarted because of this.