I just finished my Game Jam project, and it got me thinking about how much time I had to spend writing tools to make pixel movement buttery. I am thinking it might be time to revisit some thoughts I had many years ago, and formalize a feature set for a more complete set of tools that should be baked into the engine.

BYOND has been missing some pretty major infrastructure from the beginning that would make working with coordinates easy. Here's a brief list of all the things that I want to cover here:

Here's my estimation of what I've generally found myself doing that would make more serious games easier to pull off:

Native coord data type:

BYOND's pixel movement overhaul was never really fully implemented. Essentially, we added some variables to atoms, and some args to Move(), step(), etc, added bounds functions, and then called it a day in the hopes that it would make it accessible. The problem essentially, is that we're still thinking in tiles and steps, and frankly, it'd just be a lot easier to think in absolute coordinates.

Coord data types would allow us to latch on to a piece of the map and work with locations in a more accessible manner. New() still only takes a loc as the primary argument, but a coord data type would allow us to pass a map location in a single argument. It would allow us to pass information to Move() in a single argument as well, and it'd allow us to simplify the logic of working with absolute coordinates, only doing conversions back to the engine's internal tile/step format when required.

New() should take coords as an argument, which will allow developers to initialize atoms with loc, step_x, and step_y adjustments natively (turfs and areas ignoring the non-tile component, of course).

Native vector2 implementation:

At the very least, BYOND needs a native vector2 implementation. I find myself doing a lot of the same boilerplate stuff:

var/cx = cos(aim_angle), cy = sin(aim_angle)
var/gx = CENTER_X(user) + cx * (dist)
var/gy = CENTER_Y(user) + cy * (dist)
new projectile(gx,gy,user.z,cx*shot_speed,cy*shot_speed,,shot_damage)

A native vector2 implementation would make this more accessible:

var/vec2 = user.vector2(CENTER) + aim_vector

Arithmetic between coords and vector2s should be permitted.

Forced slides vs jumps

Right now, to force a slide or a jump, you have to change step_size each movement to clue the engine to how the collision system should work. This can result in developers triggering incorrect behavior and not really knowing why, because while step_size informs the engine how much something wants to move by default, it also is used to clue the engine in to the distance that an atom can do collision testing. This... Isn't great, frankly, because one of these uses for step, we generally want consistency from, but the other requires us to reactively change it based on how far we're moving the object. This leads to situations that are undesirable for the developer, and the whole system is overall really counter-intuitive.

I'd suggest that Step() and Relocate() be new forms of movement that movable atoms have declared on them, which take a variety of argument forms to simplify this problem. Step() is always a slide, while Relocate() is always a jump, regardless of step_size. Step()s to different Z levels should always be a jump.

Angular motion, vector motion, and directional motion:

Most developers just don't want to do the heavy lifting to do angular and vector motion for themselves. The math is easy to learn, but the implementation quirks of BYOND make approaching it in the engine less intuitive than the original aim for the pixel movement implementation in 490 was intended to be.

I propose the following overrides for Step() and Relocate() as implemented above:


The vector2 form should use the current z coordinate of the movable.

Collision testing:

BYOND makes collision and tracing a little harder than it needs to be. I frequently find myself writing the same logic over and over again to handle testing for collisions with atoms. While that's gotten easier with pixel movement mode and atom.Cross() finally existing, it's still a shame we don't have access to the engine's underlying collision logic in more piecemeal forms:

We need a Trace() function for movable atoms, which will use a flag-structure to perform a customized movement.

The flags would be as follows:
    TRACE_SLIDE //should trace simulate a slide
TRACE_JUMP // should trace simulate a jump
TRACE_MOVE // should the movement be attempted

//check flags
TRACE_IGNORE_CROSS //do not perform checks on Enter/Cross functions
TRACE_IGNORE_UNCROSS //do not perform checks on Exit/Uncross functions

//result flags (only with TRACE_MOVE)
TRACE_DISABLE_CROSSED // should the movement call Entered()/Crossed() hooks
TRACE_DISABLE_UNCROSSED // should the movement call Exited()/Uncrossed() hooks
TRACE_DISABLE_BUMP //should the movement call the Bump() hook

//return flags
TRACE_RESULTS //return the results in the form of a /trace datum

TRACE_SLIDE / TRACE_JUMP flags will inform the Trace() routine whether or not we need to check the space between the start and end points.

The check flags will allow us to test or perform a movement while only calling entry or exit collision checking functions. Success flags do the same thing during traces that actually cause a movement, and turn off pieces of the default behavior of movement.

By default, a trace should return what a Move() call with similar arg structure would have returned, while when using the TRACE_RESULTS flag, it should return a trace datum, which contains information on the traced movement, including a list of objects that were crossed and uncrossed, and the object that was bumped in the event of failure. The trace datum will contain a start and end coord, allowing us to determine for ourselves the distance traveled.

Using the TRACE_MOVE flag would make trace capable of more than just collision testing, but allow us to perform advanced movements. One use case, might be for instance, creating a pushing behavior in a game where so long as the object we collided with was a pushable, we use the results of the trace to call Pushed() intead of Bump(). This would help eliminate ugly implementations within Bump() requiring a tree of istype() behaviors and the embedding of separation of concern violating code all throughout the project.

A trace datum would be marked as either final or pending. A final trace datum is one where the movement was done and the hooks were called. A pending trace datum could have Finalize() called on it to mark it as final, which would complete the movement and call the hooks that would have resulted from the move in the correct order.

The Trace() function should also have an argument for a list of objects that should be excluded from collision testing, to allow us to use Trace() to determine whether or not one object can push another:

var/trace/push_trace = pushing.Trace(NORTH,step_size,flags=TRACE_SLIDE|TRACE_RESULTS)
var/trace/self_trace = Trace(NORTH,step_size,flags=TRACE_SLIDE|TRACE_RESULTS,ignore=pushing)
if(push_trace.result && self_trace.result)

This setup would also allow us to use traces to do things like force a movement, but still call the Crossed()/Uncrossed() hooks for anything that we would have passed over:


Or implement a teleport command that ignores specific obstacles:

var/list/ignoring = list()
for(var/mob/m in get_step(src,NORTH))
ignoring += m
Trace(NORTH,flags=TRACE_JUMP | TRACE_MOVE,ignore=ignoring)

The trace command should take the same flexible argument structure as the suggested Step and Relocate functions. This one command would massively simplify writing complex movement code, and interacting with the guts of the engine in a developer-friendly way that doesn't require deep, esoteric knowledge of the engine.

I've for the most part finished the basic implementation of my third rendition at a general purpose movement library. This time, I'm pretty sure I've got it all settled until some of these things get built into the engine.