ID:1467754
 
I see a lot of talk about edge sliding solutions around the forums, but not a lot of actual code to back them up.

I've spent the last few days tinkering, and I turned out an edge sliding solution that I'm quite fond of.

I've tested this at 60fps with two players across a network, and it's pretty solid. After about ten minutes of walking around on my machine, we managed to profile it out at a total of 3 milliseconds of CPU time and about 35,000 calls.

How it's done:



If you would like to play around with how this all feels, check out my demo of this in action: http://www.byond.com/developer/Ter13/EdgeSlideDemo

I use a single global collider object for the entire world to determine whether a mob can slide in one direction or another.

Basically, when the player moves, it checks to see if the player is moving diagonal, and if so, sizes a collider to the same size as the player's bounding box, and breaks the move up into two steps in either component direction for the diagonal. If either succeeds, it will attempt to move the player in the successful direction.

If the move is not diagonal, it will size the collider to half the width and half the height of the player, and move the collider into the proper corners to test if the mob is caught on a lip that is not covering greater than half the mob.

If one of these directional tests succeeds, the mob is then moved 90 degrees in the direction that succeeded.

It's actually really simple, and because the single global collider is depth-safe, we can restore it to the prior state before the slide test was called. This will make it so that it's very hard to break the slide testing by having lots of objects depending on this one global object.

This is the code for the collider:

var
list/__dirang = list(0,180,null,90,null,null,null,270)

//shortcut so you don't have to remember the list. Treat this like a proc.
#define Dir2Ang(d) (__dirang[(d)])

collider
parent_type = /atom/movable
slider
density = 1
var
atom/movable/proxy
proc
slide(atom/movable/self,Dir=0,step_x=0,step_y=0)
//might be a depth call, so store old data
var/old_self = proxy
var/old_bounds = src.bounds
var/old_loc = src.loc
var/old_sx = src.step_x
var/old_sy = src.step_y

proxy = self //used for advanced collision detection

if(Dir & Dir - 1)
//perform diagonal sliding
src.bound_width = self.bound_width
src.bound_height = self.bound_height

//resize and relocate src to the correct position
var/d = turn(Dir,-45)
locate_corner(self,Dir)

//test the first direction
. = step(src,d,2)
if(!.)
//if failed, check the second (No need to move, already in position)
d = turn(Dir,45)

. = step(src,d,2)
if(.)
//return the slide direction
. = d
else
//return the slide direction
. = d
else
//perform linear sliding
src.bound_width = self.bound_width/2
src.bound_height = self.bound_height/2

var/d = turn(Dir,-90)
//move the bounding box to the correct corner
locate_corner(self,Dir|d)

//check if we can step
. = step(src,Dir,2)
if(!.)
//if we can't step, try the opposite corner
d = turn(Dir,90)
locate_corner(self,Dir|d)

//check if we can step
. = step(src,Dir,2)
if(.)
//if successful, return the slide direction
. = d
else
//if successful, return the slide direction
. = d

//restore old data
src.proxy = old_self
src.bounds = old_bounds
src.step_x = old_sx
src.step_y = old_sy
src.loc = old_loc

//this will position the slider over the calling mob in the correct position
locate_corner(atom/movable/self,corner)
//gather the directional components and get their angles
var/d1 = corner & corner - 1
var/d2 = Dir2Ang(corner ^ d1)
d1 = Dir2Ang(d1)

//set up the position based on the bounding coverage so that the slider is on the correct corner of the movable caller
var/nx = (self.bound_width - src.bound_width) * max(sin(d1),0) + (self.x - 1) * TILE_WIDTH + self.step_x
var/ny = (self.bound_height - src.bound_height) * max(cos(d2),0) + (self.y - 1) * TILE_HEIGHT + self.step_y

//correct positioning
src.step_x = nx % TILE_WIDTH
src.step_y = ny % TILE_HEIGHT
src.loc = locate(round(nx/TILE_WIDTH) + 1, round(ny/TILE_HEIGHT) + 1, self.z)



Now, we also need to change some code in atom/movable for this to work.

Let's just give that a shot:

var
collider/slider/slider = new/collider/slider()

atom
movable
var
can_slide = 0
tmp
sliding = 0
Move(atom/NewLoc,Dir=0,step_x=0,step_y=0)
//call default action
. = ..(NewLoc,Dir,step_x,step_y)
if(!. && can_slide && !sliding)
//if this mob is able to slide when colliding, and is currently not attempting to slide
//mark that we are sliding
sliding = 1
//call to the global slider object to determine what direction our slide will happen in (if any)
. = slider.slide(src,Dir,step_x,step_y)
if(.)
//if slider was able to slide, step us in the direction indicated
. = step(src,.)
//mark that we are no longer sliding
sliding = 0


Lastly, we need to set up the world defaults to play nicely with this kind of a setup:

world
fps = 40

mob
step_size = 4
can_slide = 1


And there you have it!