ID:1774393
 
(See the best response by FKI.)
Code:
turf
WhiteBed01
icon = 'Tileset06.dmi'
icon_state = "235"
Exit(mob/M)
if(M.dir==EAST)
usr.ExitSleepLeft()
else
if(M.dir==WEST)
usr.ExitSleepRight()
else
return
Enter(mob/M)
if(M.loc==locate(src.x+1,src.y,src.z))
usr.SleepLeft()
else
if(M.loc==locate(src.x-1,src.y,src.z))
usr.SleepRight()
else
return
WhiteBed02
icon = 'Tileset06.dmi'
icon_state = "236"
layer = MOB_LAYER+1

mob
var
State = "Normal"
Doing = 0

Move()
if(usr.Doing==0)
return ..()
else
return

proc
SleepLeft()
src.loc=locate(x-1,src.y,src.z)
usr.dir = SOUTH
flick("Sleep_Flick",usr)
usr.State = "Sleep_Flick"
usr.Doing = 1
sleep(16)
usr.Doing = 0
if(usr.State=="Sleep_Flick")
src.icon_state = "Sleep"
usr.State = "Sleep"
SleepRight()
src.loc=locate(x+1,src.y,src.z)
usr.dir = SOUTH
flick("Sleep_Flick",usr)
usr.State = "Sleep_Flick"
usr.Doing = 1
sleep(16)
usr.Doing = 0
if(usr.State=="Sleep_Flick")
src.icon_state = "Sleep"
usr.State = "Sleep"
ExitSleepLeft()
src.loc=locate(x+1,src.y,src.z)
usr.dir = EAST
usr.State = "Normal"
src.icon_state = "Normal"
ExitSleepRight()
src.loc=locate(x-1,src.y,src.z)
usr.dir = WEST
usr.State = "Normal"
src.icon_state = "Normal"


Problem description:
I have made a bed icon and i want it to when you enter the bed, you go to sleep. I made that work! But now comes the problem, i made a second icon for the blankets as an overlay over the original bed icon so when you go to sleep you have blankets over you.
When i place this icon over the original icon, the procs dont work anymore of the original icon.

How can i solve this problem?

Best response
You can't put two turfs on the same tile. The bed sheets turf is overriding the bed turf and making it an underlay. Both of them should really be objects IMO.
Okay, thank you for your reply, but when i make them objects, it still does'nt work.
The reason it's not working is because Enter() and Exit() don't work the same on movable atoms as they do turfs. When a mob steps on a turf, it steps into the turf. All atoms can contain movable atoms. Turfs are special in that they are the only atoms that visibly display their contents on the map. Movable atoms can contain a list of movable atoms too. When a movable atom enters the contents list of another movable atom, it calls Enter() and Exit().

When you are moving around on the map, you are jumping from the contents list of one turf to another. Since you are never moving inside of movable atoms' contents lists while moving around on the map, obj.Enter() and obj.Exit() are never called.

What you are looking for are obj.Cross() and obj.Uncross().




*****************************************************

Also, I'd like to take some time to go over the code you posted and explain some bad habits to you.

turf
WhiteBed01
icon = 'Tileset06.dmi'
icon_state = "235"
Exit(mob/M)
if(M.dir==EAST)
usr.ExitSleepLeft()
else
if(M.dir==WEST)
usr.ExitSleepRight()
else
return
Enter(mob/M)
if(M.loc==locate(src.x+1,src.y,src.z))
usr.SleepLeft()
else
if(M.loc==locate(src.x-1,src.y,src.z))
usr.SleepRight()
else
return


There is a problem on almost every individual line of this section. I'll go after them one at a time.

1:
Exit(mob/M)
Enter(mob/M)


a) You are using Exit(). Exit() is called from Move(). It's exclusively used to test if you can enter a turf. You should always return 1 or 0 at the end of Exit()/Enter(). You should not use Exit() or Enter() to call out to behavior. You should only use Exit() or Enter() to determine whether you can successfully enter a location. Again, the reason for this is because while turf/Enter() may succeed the move itself may fail. This can cause all kinds of bugs and mismatches in data/behavior. You should be using Entered()/Exited() instead in this instance.

b) Exit and Enter have two arguments, not one. If you were using these arguments, your code would be much easier to read.

2:
if(M.dir==EAST)
if(M.loc==locate(src.x+1,src.y,src.z))


a) M.dir is not reliable in Enter()/Exit(). The object's direction may not always be the direction they are moving in. You should be using the secondary argument to determine what direction the player is moving in.

b) M.loc is reliable in Enter(). You should still be using the secondary argument for Enter()/Entered(). It's cleaner and doesn't involve an object access.

c) You never checked if M is actually a mob in either of these functions. If an obj moves into this tile you will get a nice fat runtime error because you never checked whether or not the variable M was typesafe.

3:
usr.[function]()


usr is only valid in verbs and mouse procs. Using usr outside of these scenarios can cause ambiguity that will make it difficult to diagnose errors.

mob
var
State = "Normal"
Doing = 0

Move()
if(usr.Doing==0)
return ..()
else
return

proc
SleepLeft()
src.loc=locate(x-1,src.y,src.z)
usr.dir = SOUTH
flick("Sleep_Flick",usr)
usr.State = "Sleep_Flick"
usr.Doing = 1
sleep(16)
usr.Doing = 0
if(usr.State=="Sleep_Flick")
src.icon_state = "Sleep"
usr.State = "Sleep"
SleepRight()
src.loc=locate(x+1,src.y,src.z)
usr.dir = SOUTH
flick("Sleep_Flick",usr)
usr.State = "Sleep_Flick"
usr.Doing = 1
sleep(16)
usr.Doing = 0
if(usr.State=="Sleep_Flick")
src.icon_state = "Sleep"
usr.State = "Sleep"
ExitSleepLeft()
src.loc=locate(x+1,src.y,src.z)
usr.dir = EAST
usr.State = "Normal"
src.icon_state = "Normal"
ExitSleepRight()
src.loc=locate(x-1,src.y,src.z)
usr.dir = WEST
usr.State = "Normal"
src.icon_state = "Normal"


Okay, this one's a bit better, but still got a lot wrong with it.

1) Every "usr" in this section of code needs to be replaced with src. Usr should only be used in verbs or Mouse procs.

2) Move() should return 0, not null. There's a bug in BYOND that makes a bald return ("return") very slow. Changing it to "return 0" will actually speed your code up a bit because of this bug. If you want to return nothing, "return null" also works too, but Move()'s documentation specifies that Move() should return 0 on failure.

3) SleepLeft()/SleepRight(), and ExitSleepLeft()/ExitSleepRight() should be combined into one function each. The differences between these two procs is a single character. You should be using an argument to distinguish that small of a difference in behavior. It'll save you a lot of time and potential debugging as well as keep your game smaller and easier to maintain and modify.

I'll post some fixes to your code in a minute.
First, some helper definitions: Put these in your .dme file inside of the #defines area:

#define TILE_WIDTH 32
#define TILE_HEIGHT 32
#define TICK_LAG 1 //set this to 10 divided by world.fps

#define DELAY_SINGLE -2
#define DELAY_NONE -1
#define DELAY_GLIDE 0

#define DEFAULT_GLIDE_SIZE 4 //configurable number of pixels the object should glide every frame. Should be ceil(min(TILE_WIDTH,TILE_HEIGHT)/DEFAULT_GLIDE_DELAY/TICK_LAG)
#define DEFAULT_GLIDE_DELAY 8 //how long the default movement glide should take to complete. Should be ceil(min(TILE_WIDTH,TILE_HEIGHT)/DEFAULT_GLIDE_SIZE) * TICK_LAG

//this is a lookup table. sqrt() is costly in terms of performance, so we're storing the values of the glide distance by direction.
var/list/glide_distance = list("1" = TILE_HEIGHT, "2" = TILE_HEIGHT, "4" = TILE_WIDTH, "8" = TILE_WIDTH, "5" = sqrt(TILE_WIDTH**2 + TILE_HEIGHT**2), "6" = sqrt(TILE_WIDTH**2 + TILE_HEIGHT**2), "9" = sqrt(TILE_WIDTH**2 + TILE_HEIGHT**2), "10" = sqrt(TILE_WIDTH**2 + TILE_HEIGHT**2))

/*Don't use strings for states. Use integers. Faster, smaller, and you don't have to worry about mismatches with the #define approach.*/

#define STATE_NORMAL 1
#define STATE_SLEEP 2


Alright, now that's out of the way. Let's add some stuff to make your life easier to /atom/movable:

atom/movable
var/tmp
glide_delay = 0 //how long it takes for this movable to glide from one tile to the next
move_delay = DELAY_GLIDE //how this movable must wait between movements
force_dir = -1 //force this movable to always face a specific direction
moving_dir = 0 //the current direction of movement
last_move //the last time we moved (world time)
next_move = 0 //the next time we can move (world time)
proc
//make this movable always face a specified direction
//pass -1 to turn this off
ForceDir(dir)
force_dir = dir
if(dir&&src.dir!=dir)
src.dir = dir

//use this to wait until the player stops sliding into a tile after a movement
//returns 0 if the glide completed before the player moved again
//returns 1 if the glide time elapsed
WaitForSlide()
if(glide_size<max(TILE_WIDTH,TILE_HEIGHT))
var/omove = last_move
var/glide_end = last_move + glide_delay
while(glide_end>world.time)
sleep(TICK_LAG)
if(last_move!=omove)
return 0
return 1
else
return 1

//set a delay between each movement of this object
//delay is how long you actually have to wait to move again
setMoveDelay(delay,glide_delay=null)
if(glide_delay==null)
glide_delay = delay
move_delay = delay

//set an arbitrary one-time delay for movement
//if movement is already prevented, add more to it.
//this can be ended early by calling setMoveDelay() or by setting next_move to world.time
PreventMove(delay)
if(move_delay!=DELAY_SINGLE)
spawn(-1)
var/nm = world.time + delay
if(next_move>=nm)
return
var/omd = move_delay
next_move = nm
move_delay = DELAY_SINGLE
while(world.time<next_move)
sleep(TICK_LAG)
if(move_delay!=DELAY_SINGLE)
return
move_delay = omd
else
next_move += delay

Move(atom/NewLoc,Dir=0,step_x=0,step_y=0)
//prevent movement if a delay is currently set
if(move_delay!=DELAY_NONE)
if(next_move>world.time)
return 0
moving_dir = Dir //store the moving_dir for situations where direction/enter/exit locations are unreliable
. = ..()
if(.) //if the supercall was successful, perform post-move events
//store information about the last move and the next move
last_move = world.time
if(move_delay>DELAY_NONE)

if(move_delay==DELAY_GLIDE)
next_move = world.time + round(glide_distance["[moving_dir]"] / (glide_size==0 ? 4 : glide_size)) * TICK_LAG
else
next_move = world.time + move_delay
//if we're forced to keep a specific direction, set it after this movement
if(force_dir&&dir!=force_dir)
dir = force_dir


Now let's take a look at some of your code. Your state management needs a quick rework:

/* doing shouldn't just be a boolean. Make it a bitfield.
It can store 16 values at a time this way. Whenever you need a new "capability"
the player can or can't do, just add another define like the ones below
with a value multiplied by 2 of the last one: 1, 2, 4, 8, 16, 32, etc... */


#define DOING_NOMOVE 1
#define DOING_NOATTACK 2

mob
var
state = STATE_NORMAL
tmp/state_entry //the time we entered this state
doing = 0
Move()
if(doing&DOING_NOMOVE) //this is the fast way to check if you can do something.
return 0
return ..()
proc
//determine whether we can do something
//you shouldn't need to call this on your own
//multiple flags can be tested at once like this: (DOING_NOMOVE|DOING_NOATTACK|DOING_WHATEVER)
canDo(flags)
return !(doing & flags)

//manage the current state
//state should be one of your STATE_[name] defines
//flags should be a bitfield of all the flags required to be free to enter the state
//setting multiple flags at once should be done like this: (DOING_NOMOVE|DOING_NOATTACK|DOING_WHATEVER)
//if you want to force the player to enter the state, set the optional force parameter to 1
setState(state,flags,force=0)
if(force||canDo(flags))
src.state = state
src.doing = doing
src.state_entry = world.time
return 1
return 0

//sleep for [time] ticks as long as the current state has not been changed
//returns 0 early if the state is changed
//returns 1 if the sleep finishes on time
StateSleep(time)
var/entry = state_entry
time += world.time
while(time>world.time)
sleep(TICK_LAG)
if(entry!=state_entry)
return 0
return 1


Okay, notice a few of these neat little convenience functions I put together based off what I saw you doing.

Now managing states should be really easy and require a little bit less code. On top of that, it's more future-proof as your game gets bigger.

Now that we've got all of that done, we can change your code to this:

mob
proc
SleepEnter(beddir)
setState(STATE_SLEEP,STATE_NOMOVE,1) //set the state to the sleep state
WaitForSlide() //wait for the player to stop sliding into this tile
ForceDir(beddir) //make sure the player can never change directions while in bed
src.icon_state = "Sleep" //we can set the final icon state before we do the flick, which will have less of a graphical delay.
flick("Sleep_Flick",src)
if(StateSleep(16))
doing &= ~STATE_NOMOVE //allow the player to move again

SleepExit()
src.dir = moving_dir
WaitForSlide() //wait for the player to stop sliding out of this tile
ForceDir(-1) //allow the player to change directions again.
setState(STATE_NORMAL,0,1) //return to the normal state

//useful for hard-coded overlays attached to objects so you can save your mappers some headaches later.
//also helps improve performance.
overlay
parent_type = /atom/movable
WhiteBed
//don't forget to set up your icon here
layer = MOB_LAYER+1

obj
bed
overlays = list(/overlay/WhiteBed) //this won't show up in the map editor, but it will show up in game
Cross(mob/M)
//check if the entering object is a type of mob
if(ismob(M))
//beds can be entered from +/- 90 degrees of their facing direction
var/dir1 = turn(src.dir,90)
var/dir2 = turn(src.dir,-90)
//check if mob is moving in from the sides of the bed.
if(M.moving_dir==dir1 || M.moving_dir==dir2)
//allow entry
return 1
//otherwise, disallow entry
return 0
//if the crossing object isn't a mob, do the default action
return ..()

Uncross(mob/M)
//check if the entering object is a type of mob
if(ismob(M))
//beds can be exited toward +/- 90 degrees of their facing direction
var/dir1 = turn(src.dir,90)
var/dir2 = turn(src.dir,-90)
//check if mob is moving in from the sides of the bed.
if(M.moving_dir==dir1 || M.moving_dir==dir2)
//allow exit
return 1
//otherwise, disallow exit
return 0
//if the uncrossing object isn't a mob, do the default action
return ..()

Crossed(mob/M,atom/oldloc)
if(ismob(M))
//allow the Move() proc to finish before calling dependent procs
spawn(-1)
M.SleepEnter(src.dir)
..()

Uncrossed(mob/M,atom/newloc)
if(ismob(M))
//allow the Move() proc to finish before calling dependent procs
spawn(-1)
M.SleepExit()
..()
Thank you very much, for the explanation and for the effort of making the correct lines of code. I implemented it and it works just fine had only one error at the proc 'sleepenter' with the STATE_NOMOVE which needed to be DOING_NOMOVE and further it worked good. :)
I have one little question more, i'm not sure how to set the movement speed higher because with this implemented the character walks very slow now. Which variable should i edit ?
I didn't read the script. But, my guess would be the standard mob.step_size variable.
Found it, i had to set the TICK_LAG to 0
In response to Chaokai
Chaokai wrote:
Found it, i had to set the TICK_LAG to 0

Don't set TICK_LAG to zero. Set TICK_LAG to 10/FPS. Whatever you have your world FPS set to by default, use that as the divisor of 10 to get your TICK_LAG value.

If you want to be able to change world.tick_lag at runtime, change the TICK_LAG define to be world.tick_lag. If you have a static TICK_LAG, though, it's best to set it to the number based on FPS.
In response to 8BitParagon
8BitParagon wrote:
Chaokai wrote:
Found it, i had to set the TICK_LAG to 0

Don't set TICK_LAG to zero. Set TICK_LAG to 10/FPS. Whatever you have your world FPS set to by default, use that as the divisor of 10 to get your TICK_LAG value.

Or as a time saver, you can also set world.fps directly; it's basically there as an easier-to-use counterpart to tick_lag.
#define FPS 40
#define TICK_LAG 0.25

world
fps = FPS


I don't think I understand your reasoning. FPS isn't really any easier to use than TICK_LAG, as they are both reciprocal to one another:

    tick_lag = 10/fps
fps = 10/tick_lag


Waiting an arbitrary amount of time (measured in 1/10ths seconds):

    sleep(10) //wait 1 second


Obviously, the above example should always be used if you want to maintain timer consistency as much as possible.

Waiting an arbitrary number of render-frames:

    sleep(world.tick_lag*4) //sleep for 4 frames


    sleep(40/world.fps) //sleep for 4 frames


Which one of the two is more readable? Not only that, but since DM is written in C we're victim to the optimization of DM itself:

http://stackoverflow.com/questions/17883240/ is-multiplication-faster-than-float-division

DM's arithmetic operators are abstractions and as such have to use non-literals as operands. In other words, multiplication will always be faster than division in DM.

Not only is using fps to determine sleep less readable, but it's slower.


Why I recommend my approach:

1) It makes changing your projects world.fps and world.tick_lag much easier. You can update every ticker-dependent sleep by changing a single define.

2) My approach uses a compile-time constant provided that your world.tick_lag doesn't change while the game is running. Otherwise, the preprocessor macro needs to be given the value of world.tick_lag, rather than a constant. This ensures the minimum impact of all frame-locked sleeps.

If no fps changes are possible:
#define FPS 40
#define TICK_LAG 0.25

world
fps = FPS


If fps changes are possible:

#define FPS world.fps
#define TICK_LAG world.tick_lag

world
fps = 40


To perform a frame-dependent sleep in both cases:
    sleep(TICK_LAG * 4) //sleep for 4 frames


LummoxJr wrote:
Or as a time saver, you can also set world.fps directly;

My approach takes no more than 20 seconds to set up. It can save you hours in debugging and refactoring later. I'd argue the approach is vastly superior to any alternative that can be offered.