I'm a programmer. As such, I am lazy. I also help people a lot on these forums. But I'm also lazy.
This is a conundrum. To solve this, I have decided to write a series of tutorials regarding common questions on these forums, so that I can link to them for convenience. See, I already do this quite often, but also quite often I have to expand on the topic. Thus, I wish to make detailed, instructional tutorials that will need little more expansion.
My first topic is on teleportion, and specifically door-like teleportion. That is, when something steps on a tile and is immediately transported to another place. This is a relatively simple topic, but one that can have numerous errors associated with it, despite the fact there is one well-known, fairly-uniform, simple result.
To start off, let's look at where things can go wrong.
How Not To Do It
First off, let's take a look at the wrong way to do it. Below is extremely simple, but it is also wrong in a good number of ways.
m.loc = locate(4, 30, 1)
There we go. Simple enough, right? When something tries to enter the turf, their location is to the location at (4, 30, 1). Only two lines, so this seems like a good choice, right? Wrong. In just two lines, I can identify four major flaws: It's not robust, it's not dynamic, it's not easily-modifiable, and it does not follow the interface set out for it.
What do I mean by robust? I mean it doesn't handle errors well. There are two very simple examples in this code. The first is the assumption that the argument is in fact a mob. While it will also handle well whether it's a mob or an obj, it will typically not handle well for anything else. For example, if somehow a turf or area were to be passed as arguments, this would crash, as their loc variable can not be changed at runtime. Additionally, if it happens that the object at (4, 30, 1) doesn't exist (perhaps the map was changed). This won't cause a runtime error, but it will likely delete the object being moved. If this object happens to be the player's mob, then they'll also be disconnected! Not good.
What do I mean by not dynamic? In order for this method to work, there has to be a specific object created for every possible door. If you want to go just one block over, for example, to (4, 31, 1), you have to create an entirely new object. This is ridiculous! There is no need for a new type of object for every door that can exist. There must be a better way.
What do I mean by not easily-modifiable? Well, assume you rearrange your map. Now your old coordinates don't work. You have to go through every object in your code and change their coordinates. On top of that, because it is not dynamic (as mentioned above), you also might have to delete a bunch of objects (if you don't want your code to get bloated).
Lastly, what do I mean by the code not following the interface set out of it? This is the most difficult to explain. Under the normal system of movement in DM. The Enter() proc should only ever take a movable atom (/atom/movable) and return true or false as to whether it can move to that location. The code within the Enter() function should not actually have any effect on the world, just return true or false as to whether something happened. The Entered() method, instead, should be used when something has successfully entered the turf. This is the interface that BYOND sets out for these procs. Other languages have stricter ways of enforcing an interface, but DM unfortunately does not. Thus, there are a lot of cases of people messing it up. To follow the interface, this type of code should actually be moved into the Entered() function.
Well, now we know a few things. We need to use Entered(). We need to make it more robust and capable of dealing with potential errors. We need to make it more dynamic so we have less useless repetition. We need to make it more easily-modifiable, so that we don't need to go through a lot of work when we want to change something. How can we manage this?
Well, below is one possible way, which may have occurred to you.
// The x, y, and z coordinates of the turf you're being
// transported to.
// We're using Entered() now, to follow DM's interface
// for movement.
// Check that m is a mob,
if(locate(to_x, to_y, to_z) != null)
// If locate moves the player to a valid location,
// then move them.
m.loc = locate(to_x, to_y, to_z)
This seems like a much better solution, doesn't it? It uses Entered(), so BYOND's movement interface is preserved, we do type checking and value checking to help prevent errors, and we now have variables that allow us to be a bit more dynamic with our objects, keeping down bloat, and it makes it a lot easier to modify a map. Now, instead of setting the coordinates inside the code, we can just use the map editor to edit a turf's to_x, to_y, and to_z variables. The downside is that we're still stuck to our coordinate system, so if we re-arrange the map we might have a lot of coordinate sto change. Still, this seems like it could be the best we could do without getting a lot more complex, doesn't it? Well, not quite. There's a feature of DM that makes this a bit simpler.
The tag Variable
The tag variable is a special variable in DM that is used in conjunction with the locate() proc. If you set the tag variable of an object, then locate(tag) will return that object. Note that tag can not be set at compile-time, and multiple objects should not have the same tag variable. Thus, using the tag variable we can separate our system from the coordinate sysetm of the map, simplifying re-arranging it.
How To Do It
Taking all we have, we can now produce the following code.
// This is the tag of the location we're moving to.
// This stores the turf we're moving to. Note that tag
// isn't only on turfs, but all /atom types. Therefore,
// it's entirely possible that this is not a turf.
// I do not typecheck here, though, because it is
// up to the map designer to put a tag on the right
// object, and they could have a reason for moving
// the player to a non-turf.
var/turf/t = locate(moveto_tag)
a.loc = t
And there we have it. I've changed mob/m to atom/movable/a to allow /obj objects to pass through the door now, too. This gives us the most robust, most dynamic, most easily-modifiable solution that fit's BYOND's movement interface and is, at the same time, simple. This is the solution that the average user needs. Now, in order to set up a 'portal' between two doors (or whatever you have), you just need to edit the entry point's moveto_tag variable, and the exit point's tag variable.
Beyond The Basics
We could conceivably go well beyond this, though. For example, under the standard BYOND movement interface, when movement occurs, it is because the moving object's Move() proc was called, with their new location as the argument. Then the current location and the new location determine whether the user should move. Rather than directly modifying the moving object's loc variable, we could use the Move() proc to double-check whether movement should occur or not, or how it should occur. This could be used, for example, to implement traps, locked doors, or things blocking one or both ends of the door. Remember, BYOND's default movement interface, while lacking in features in a few places, is still very robust and useful, and it's a good idea to use it when you can.
It's also possible that you may want to use movable atoms rather than a turf or area as a portal. Because the Move() proc will never call the Enter() or Entered() procs, this has to be implemented in a different, but similar, way. There are two procs exclusively for the /atom/movable type that acts similarly to Enter() and Entered(), called Cross() and Crossed(). The implementation are very similar.