ID:281611
 
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.

turf/Enter(mob/m)
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.

Getting There

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.

turf
var
// The x, y, and z coordinates of the turf you're being
// transported to.
to_x
to_y
to_z

Entered(mob/m)
// We're using Entered() now, to follow DM's interface
// for movement.

if(istype(m))
// 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.

turf
// This is the tag of the location we're moving to.
var/moveto_tag

Entered(atom/movable/a)
if(istype(a))

// 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)
if(t)
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.
Well, that was a ridiculously long explanation to get a few lines of "correct" code.
-That if(istype(a)) doesn't seem like what you'd want. Allowing non-mobs (generally non-player-mobs) to enter doorways is usually undesirable.
-You never really explained how to set the tag variable.
-Implementing doorways as /turfs can be somewhat flunky, since all turfs in a single tile merge into a single entity. Now that the Cross type procs have been implemented, you can effectively use /objs for doorways.
-Building doorway code into Enter() is acceptable. Entered() could allow for potential blocking of doorways, which isn't necessary desirable. Enter() vs Entered() in your code would provide essentially identical results.
-Your code would also result in an instant movement as soon as they "touched" the doorway, which is generally a poor looking effect. If you're going to use Entered(), you may as well implement a graphically functional delay.
In response to Falacy
Falacy wrote:
Well, that was a ridiculously long explanation to get a few lines of "correct" code.
-That if(istype(a)) doesn't seem like what you'd want. Allowing non-mobs (generally non-player-mobs) to enter doorways is usually undesirable.

Then you use the magic of overwriting the method and calling the parent proc. I start with the most free case, which can then be restricted. It is not possible to be restricted and then go to a freer case. Thus, my generalized solution is the best here. If you wish to change it, it is as simple as follows.

turf/newturf
Entered(mypath/foo)
if(istype(foo))
..()


If I were to choose the more restrictive choice, then the only option is to rewrite all of the code. This is unnecessary.

-You never really explained how to set the tag variable.

This is a valid mistake, which I will edit in.

-Implementing doorways as /turfs can be somewhat flunky, since all turfs in a single tile merge into a single entity. Now that the Cross type procs have been implemented, you can effectively use /objs for doorways.

The caveat with this method is that BYOND's default movement interface never results in the Enter() proc being called for any type of /atom/movable. Consequently, this would require a change to the Move() proc in /atom/movable, which is a much more involved modification.

-Building doorway code into Enter() is acceptable. Entered() could allow for potential blocking of doorways, which isn't necessary desirable. Enter() vs Entered() in your code would provide essentially identical results.

Building this sort of code into Enter() is functional, yes. But it should not be considered acceptable, as it breaks the interface. Using the terminology of other languages, if BYOND were strictly-typed, the Enter() method should be bool, and the Entered() method should be void. Entered() should be an event handler, and Enter() should be whether that event can even occur.

While I was overstating it in the tutorial, the affect of Enter() on the world should be minimal, at best. The most I could see it doing is alerting someone than an attempt to Enter() was made. Even in this case, I believe this would be bettered handled by something like a NoEntered() or EnteredFail() event (one of the things I believe the basic interface is lacking), rather than it actual occurring within Enter().

-Your code would also result in an instant movement as soon as they "touched" the doorway, which is generally a poor looking effect. If you're going to use Entered(), you may as well implement a graphically functional delay.

Once again, I do not know whether the user would want to implement this, or how they would prefer to implement it. I am not going to make a fumbled attempt at implementing something I can not predict, but I can implement it in a way that it can be usefully overridden. As above, all the programmer has to do is wait until they prefer, and then call ..() in the Entered() proc they overwrite, and the effect would be preserved. Thus, making an attempt at implementing it is both useless and outside the scope of the tutorial.

[Edit]

On top of all that, this is a tutorial, not a library. I am demonstrating and guiding a user through the process to implement these things, not implementing it myself. It is up to the user to decide what exactly they need, and how to implement it.
In response to Popisfizzy
Popisfizzy wrote:
The caveat with this method is that BYOND's default movement interface never results in the Enter() proc being called for any type of /atom/movable. Consequently, this would require a change to the Move() proc in /atom/movable, which is a much more involved modification.

The Cross type procs work on movable atoms in essentially the same same way that Enter type procs would on a turf. I have found them to be useful, and somewhat ridiculous that they weren't added before pixel movement. You can use them to easily implement things like pushing objects, allowing players to walk through each other, triggering events, etc.
In response to Falacy
Falacy wrote:
The Cross type procs work on movable atoms in essentially the same same way that Enter type procs would on a turf.

Interesting. I rarely use DM anymore, and thus was not aware of this addition. Perhaps in the future I'll make a demonstration or tutorial involving it, but it's currently beyond the scope of this tutorial. In any case, I'll make a note in the last section about it.
so for the code
 turf
// This is the tag of the location we're moving to.
var/moveto_tag

Entered(atom/movable/a)
if(istype(a))

// 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)
if(t)
a.loc = t
lets say i wanted to make it so that if you entered a icon state i made called
entrancel
icon = 'person.dmi'
icon_state = "castle-entrancel"
what do i change in the code to make it so you are teleported on the same spot on the second layer in the map
In response to Disasterousclown
It is extremely clear you are in over your head. You need to start here or here, and forget where you're at now. You don't have near enough basic knowledge to attempt any of this relatively-basic stuff.
The problem with using turfs is that you're binding the warp behavior to the turf type. If you want to change the map to make the ground be dirt instead of grass, you need to update the warp turf. I wouldn't consider being a warp a property of the turf, I'd rather use areas (or invisible objects) for this type of thing. This way you can place them on any turf to turn that turf into a warp.

area
warp
Entered(mob/m)

// the destination is the turf in this area
// that the mob isn't standing on.
var/turf/destination
for(var/turf/t in src)
if(t != m.loc)
destination = t
break

m.loc = destination

warp_01
warp_02
warp_03

You can give the warp areas an icon (that is set to null in their constructor) so you can easily see them on the map. I find it a lot easier than managing tags.
Creating that extra destination var and then moving the mob after the loop is ugly. You could set m.loc=t in the loop. Also, you should throw an ismob(m) type check in there (and maybe return ..()), since you know noobs are gonna copy/paste this code and hope it works =P
Actually, to fix it up, I'd do it like this.
In response to Falacy
Falacy wrote:
Creating that extra destination var and then moving the mob after the loop is ugly. You could set m.loc=t in the loop. Also, you should throw an ismob(m) type check in there (and maybe return ..()), since you know noobs are gonna copy/paste this code and hope it works =P

He could also do it like this:
var area/a = target_type ? (locate(target_type) in world) : src
m.loc = locate(/turf) in (a.contents - m.loc)

But that's just hard to read. Readability is important.
In response to Kaiochao
Kaiochao wrote:
Readability is important.

That was my point, I'm not sure why you're trying to post nonsense with no context.

His library is pointlessly overcomplicated, barely readable, and still has no type checking.
In response to Falacy
Falacy wrote:
Kaiochao wrote:
Readability is important.

That was my point, I'm not sure why you're trying to post broken nonsense with no context.

His library is pointlessly overcomplicated, barely readable, and still has no type checking.

1. The context of my code is his library (specifically, in destination()), and it works.
2. Type-checking isn't very important if only movable atoms can enter areas.
3. If anything, the overcomplicating parts of his library make it even more readable.
It's much easier to understand that "destination" means destination, rather than "t".
In response to Kaiochao
Kaiochao wrote:
1. The context of my code is his library (specifically, in destination()), and it works.
Inserted into a specific point of his library, perhaps. However, they don't have that context in your post, where they are just 2 random lines.

2. Type-checking isn't very important if only movable atoms can enter areas.
Entered() is a proc like any other, people could call it with whatever parameters they wanted.

3. If anything, the overcomplicating parts of his library make it even more readable.
It took him over 50 lines to accomplish what should be a perfectly readable example in less than 5.

"destination" is easier to understand to be a destination than "t", of course.
"t" being a poor variable name doesn't make a destination proc any more necessary or readable. "target" isn't a good variable name either, since it doesn't properly describe what is being referenced, and is later named "target_type" in the destination proc.
In response to Falacy
Falacy wrote:
His library is pointlessly overcomplicated, barely readable,

Those things don't always matter. What's most important is the interface that the library provides - how easy it is for developers to make use of the library to suit their needs. Sometimes the internals of the library have to be complicated to accommodate all the ways people might want to use it. If you only cared about making a single warp that moves you to another location, you'd do this:

turf/Enter(mob/m)
m.loc = locate(4, 30, 1)

It's certainly readable and isn't overcomplicated =)

and still has no type checking.

The library makes no assumptions about what objects can or cannot use warps. That's not something the library would do, but it's something you can easily implement using the library:

warp
// player-only warps:
player_only
warp(mob/player/p)
if(istype(p))
..()
In response to Forum_account
Forum_account wrote:
It's certainly readable and isn't overcomplicated =)
It also isn't functionally reusable. When you have 20 different doorways using that method, it becomes a lot less readable, due to the amount of repetitive spam code you would need.

The library makes no assumptions about what objects can or cannot use warps. That's not something the library would do
The issue isn't about the library limiting functionality, but preventing errors.

There is also a problem with both of these examples; if you want to have a doorway that is more than a single tile.
In response to Falacy
Falacy wrote:
Entered() is a proc like any other, people could call it with whatever parameters they wanted.

If it's going to account for stupidity, how do you plan on stopping them from overriding the proc?
I think type checking would be bad, I would prefer them to get an error message letting them know they screwed up.

Falacy wrote:
It took him over 50 lines to accomplish what should be a perfectly readable example in less than 5.

I happen to like his example, but I would prefer it if he buffered the destination so it didn't have to look for it every time.
In response to Falacy
Falacy wrote:
It took him over 50 lines to accomplish what should be a perfectly readable example in less than 5.

The demos are what counts. If the library was only 5 lines it couldn't be as flexible and the demos would be more complex. The goal is to keep the demos (or whatever project uses the library) as simple as possible.

Also, lines of code isn't a good measure of complexity (consider Kaiochao's 2-line example above). You're also counting whitespace and comments towards that total, it's really only 25 lines of code.
In response to Forum_account
Forum_account wrote:
The demos are what counts. If the library was only 5 lines it couldn't be as flexible and the demos would be more complex. The goal is to keep the demos (or whatever project uses the library) as simple as possible.
None of it matters, really. The noobs who would need to use a library for something would never bother to actually read through it anyway.

Also, lines of code isn't a good measure of complexity (consider Kaiochao's 2-line example above).
His code isn't a good example of complexity because it is out of context (would take several more lines to even compile that code), and is an intentional example of pointlessly unreadable code designed to save space without improving functionality or efficiency.

You're also counting whitespace and comments towards that total, it's really only 25 lines of code.
The awkwardly placed whitespace, and unnecessary comments certainly contribute to the poor readability.
In response to Falacy
Falacy wrote:
There is also a problem with both of these examples; if you want to have a doorway that is more than a single tile.

For a two-tile wide door, create two warps.

One catch would be having a door that's shown as being two tiles wide on the outside but one tile wide on the inside. I considered making a version of the library that uses the Region library, which would make it easier to handle things like that (it'd also avoid other problems).

I would like to add other default types of warps to the library. The current one, a simple back-and-forth between two tiles, is sufficient for a lot of things. There are some things that are quite a bit out of reach. Suppose you wanted to link edges of two z levels together so it behaved like they were adjacent. You wouldn't want to create 100 warp types to place along the edge of a 100x100 map. I'm looking to find ways for the library to easily support these kinds of warps in a generic way.
Page: 1 2