Welcome to the return of snippet sundays. I'll only be doing these infrequently from here on out, and I'm only going to be showing off things that I myself find particularly interesting. If you have an idea for a snippet Sunday, or a particular problem that "BYOND can't do", bring it up and maybe I'll see about taking a look at the problem on a lazy Sunday in the future.
Anywho, today's writeup is going to be all about /image objects.
What are they?
/image objects are a purely visual atom, which act sort of like a cross between normal movable atoms and overlays. Normally, when a movable atom is in the contents list of another movable atom, it doesn't render anywhere. Images are unique in that they can be assigned a location of another atom, and appear on top of their location as a visual effect. Images can also be used on top of areas and turfs. One thing that not many people know, is that an area will render at every turf location that is included in the area's contents list, effectively allowing you to render a single image object in multiple places at once.
What can you do with them?
Image objects allow you to control which players have the ability to see them. You can also use image objects to override the visual appearance of an object completely. Let's take a look at one particularly interesting use of this approach: Rooves.
There are a number of roofing libraries on BYOND that all do roughly the same thing:
The abysmal:
http://www.byond.com/developer/PirateHead/rooflib
This roofing library has so much wrong with it I can't even begin to detail it all. Basically, it uses at least nine image objects per client, and adds them all to the client when they can be seen, rather than hiding only a single roof at a time. On top of that, image objects are deleted using the del instruction, rather than simply removed. There's even more wrong with it, but I won't go into it all here. Needless to say, stay away from this approach as though it were the plague.
The bad:
http://www.byond.com/developer/Siientx/RoofingLibrary
This roofing library uses invisibility, rather than image objects. This means that you can't use the invisibility variable for anything but roofing. Therefore, it can't be used if you plan on using invisibility for anything else. It is, however, incredibly simple.
The mediocre:
http://www.byond.com/developer/Shadowdarke/RoofLib
This roofing library uses images, which is great. Unfortunately, Shadowdarke's library adds all roof images that the player is currently able to see to the images list of the client. I'll detail why this is bad further on. Also, ShadowDarke's roofing library is missing some features that would make it truly great, Such as being able to customize roofs on the map, and being able to have roofs use more than a single image for the entire area.
As for why adding the images for all roofing tiles to the client's view list when they are visible, this is bad because every frame for every client, the server checks for changes to any image in the client's images list. This will cost you a chunk of CPU, and it makes the images list of the client just a bit harder to use.
The good:
So, we're going to spend some time putting together a roofing library that doesn't suffer from any of these weaknesses.
Let's outline what the roofing system SHOULD do before we get started building it.
1) It should use no more than a single image object per unique roof tile.
2) It should be paintable on the map.
3) It should not require placement of special objects that determine the entry points and exit points to the building.
4) It should not require the user to paint a bunch of complex areas on the map to get it to work.
5) It should only use the images list for the single roof that we are NOT showing, rather than for all of the roofs we ARE showing.
Now, #5 might give you some pause. "But Ter, we can only use the images list for images the player CAN see! Players can't see the images by default!". Well, I hate to break it to you, but that's a faulty assumption and it's plagued just about every system I've ever seen written that uses /image objects since 2002.
Per usual, I'm here to explain to you why everything you know is wrong. Let's get started, shall we? (To be continued in two more posts)
ID:1797736
Mar 1 2015, 6:15 pm
|
|
One thing that should be noted is that most if not all preexisting roofing implementations predate the introduction of image.override. Therefore the image-based implementations tend towards "positive" forms where all roofs are all /image objects that get added to the clients and removed as needed, rather than "negative" ones where the roof is always visible until an image overrides it.
|
All of them do. I'm not sure anyone is actually aware of image.override. I've never seen anyone actually use it.
|
Alright, this took a lot longer than I thought it was going to due to a couple of weird BYOND bugs. The code is up. A video showing how I construct these roof objects will Be coming up shortly. For now, here's a nice little gif:
Special thanks to Lige and MDC for volunteering to help me test. (That's MDC in the gif) |
You mentioned running into bugs, but what were they? The only thing I saw specifically was that you said there was flicker from animate(), but I need more detail than that.
|
This is great! It's not as abstract as the previous Snippet Sundays. It gets right to the point of explaining how to implement an actual, useful feature for games.
I will admit that I overlooked the usefulness of image.override. I never really thought about ways that it could be exploited like that. This lesson made me realize that the image.override feature has many interesting applications, so thank you for that. I hope to see more snippets like this one. |
Yeah, the abstract in this one was dominated by the example I used to show how to use the little abstract features I wanted to teach (appearance/overlay list unique id references and the override variable).
Turns out that atom.override was only implemented in 2011. I thought it was a few years older than that. |
Beautiful roofing system. I'll have to try and remember to study on it awhile later to see how exactly you did it; I'll probably learn something useful. As for image.override, I think I've used it once in the past as an admin verb to make me invisible to a selected player (for goofing around). Other then that, it's quite difficult to think of a useful way to use it, although I get the gut feeling there are several. I suppose that's just something I'd need to make a complete game in order to find.
|
I've used it a bunch of ways.
I wrote a system that uses it just the other day for floating damage numbers. The numbers show up in orange for observers to the attack, the numbers show up in yellow for the person who did the attack, and the numbers show up in red for the person on the receiving end of the attack. --There are a ton of good uses for the feature. |
In response to Ter13
|
|
Ooh, that's a neat trick.
I always envisioned something like image.override as a disguise feature, something you could use for a game like MLAAS for instance. In my very first BYOND game I thought it might be interesting to disguise a mob, but couldn't think of a way to do it without giving the images to everyone. |
I always envisioned something like image.override as a disguise feature, something you could use for a game like MLAAS for instance. In my very first BYOND game I thought it might be interesting to disguise a mob, but couldn't think of a way to do it without giving the images to everyone. I had the same thought at first when I first discovered it. Out of curiosity, does the name of the override image show up in the status bar on hover, or does that always use the name of the atom? I never keep the status bar around in my interfaces, so I don't know. If the name of the image shows up, it could be really effective for disguises. |
Alright, so let's get started. First, let's talk about what this system is going to do.
This system is going to allow you to paint objects on the map as a means of defining roof tiles. On initialization, if there are multiple of these objects in a single tile, they will merge into a single roof object by adding their appearances to the root roof object's overlays list. Objects that have been merged with the root are then moved to null location, where they will be garbage collected.
After the map has been initialized, world/New() is called. We loop through all of the tiles that were flagged as having roof objects and use a clever trick to generate a unique id based on the roof object's overlays list. If the current area object that the roof is located within doesn't already have a copy of that unique overlays list, we subdivide it, creating a new subarea. If the area already has a subarea with the id matching the unique overlays list of the roof object, simply add the current turf of the roof object to that subarea. The subarea's overlays list will be set to the roof object's overlays list, then the roof object will be sent to a null location where it will be garbage collected.
Areas and subareas will require some reworking of the default area behavior. We need to make it so that multiple different area instances will behave as though they were a single area instance. We do this by creating subdivision behavior that links the instances by a root area. Subareas will act as proxies for the root area, and call the root area's Enter()/Exit()/Entered()/Exited() procs instead of their own. Further, Entered()/Exited() will return zero by default if the referenced movable is moving between areas that are subareas of the same root area, or between the root area and one of its subareas.
Lastly, when a player enters a root area, it will use another clever trick to hide the area with a nice little fading animation.
Clever trick #1 explained:
The first clever trick I mentioned requires a little explanation. First of all, I talk about "appearance abuse" quite a lot in my rantings. Understanding what appearances are and how they work is key to this particular trick.
In BYOND, all atoms and images have a thing called an appearance. Appearances are unique. Meaning no two appearances can be the same. If two objects are given the same visual properties (icon, icon_state, color, alpha, pixel_x, pixel_y, pixel_z, transform, and a few others), the two objects will actually share a reference to the same appearance object on the server. The only place you can access appearance information is actually in overlays and underlays lists. Each entry in an overlay/underlay list is actually an appearance. This is why you can add types, strings, objects and icons to overlays: It's because they actually convert all of the operands of the add operator to appearances. This is also why you sometimes get "stuck" overlays/underlays, because when you change the visual appearance of an object, the appearance reference of the object changes and subtracting that object doesn't remove the old appearance, only the new. This particular quirk is actually quite useful as I showcase in my "appearance abuse" snippets.
Overlay/underlay lists are actually unique as well. Identical overlays/underlays lists share the same ID as each other.
As you can see in the snippet, each time you add something to the overlays list, it it changes reference provided it has more than a single user.
You can also see in this snippet, that each time you change the object's appearance, you wind up with a new appearance reference.
This is the crux of the trick that we're going to be using. We can generate only as many unique subareas as needed based on the reference id of the overlays list.
Clever Trick #2 explained:
The second clever trick allows us to hide objects using images. There exists a variable for images called "override". Override makes the image hide the default appearance of the object the image is attached to. This effectively makes a "negative" image possible, in that you can add a blank or otherwise invisible image to an object and show it on a per-player basis to hide an atom from a player.
This is the trick that gets us around the common approach of showing all roof areas in the world to the player on login. Instead, we just give the areas default appearances that are always visible, and then when the player enters a building, we hide only that building's roofing areas from the player using an override image.
Breaking areas:
Alright, let's get started. First, we need to modify areas a bit to suit our purposes. We need to override the Entry/Exit functions and set up behavior that makes a subdivided area act as though it were a single large area. Beware, this breaks normal area behavior. This breaks two things: 1) Checking if an area is the same as another may result in inconsistent behavior if you want to consider subareas as the same as their root or sibling areas. 2) Getting the contents of an area will not return everything in subareas.
We're going to add two functions that will fix these two problems. isSame(area/a), and getContents(). Use these two functions instead of the normal way you'd go about things if you want to use this library.
The roof object:
The roof object should be pretty simple to set up. All it needs to do is combine itself with earlier roof objects. We're going to store the flagged turfs in a temporary global list called __building_rooves, which will be processed during world/New().
The construction loop:
Now that we've got roof objects implemented, let's work in the construction loop. What this will do, is loop over the __building_rooves list and subdivide the areas that roof objects are sitting in, creating only the number of area subdivisions necessary. We will use __unique_rooves as a global list storage to keep track of unique area/roof combinations temporarily. At the end of the construction loop, we will clear both of these global lists, freeing up their memory.
Pretty animations:
Okay, now that we've got all the structure in place, we need to actually do the hiding of the roof objects. Let's create a datum that will perform the animation for us. The reason we want a datum to do the handling for us, is because I don't like polluting the mob or client with lots of variables. This structure is much easier to read, and since our animation can be interrupted part-way, we need to store a few bits of information about the fade animation, like when it started, so we can calculate the alpha value to reverse the fade from. This will result in really smooth looking fade animations.
Note that the above code snippet got a little ugly because I had to work around a few BYOND bugs while I was working. I'd prefer not to iterate through the lists more than once, but because of the way that animations are sent to the client, animations that are sent on the same frame as object creation or appearance changes related to the animation cause visual flicker.
Next, all we have to do is modify /area a bit and we're home free:
There you have it. That's the entire library. You can now go into the map editor and start plopping down your roof objects on top of buildings. Make sure you create a new instance of area for each building that you want to have separate rooves.
I'll be posting a quick video tutorial on how to use this system in a little bit. My first attempt at it took half an hour and was fairly shitty. I'll be doing a really fast overview this time around, so look for it.
Also, a few tricks here. Rooves are objects, so you need to memorize some shortcuts within DM's map editor to work with them efficiently:
Shift+left click will delete the topmost object at the location you are clicking on.
Ctrl+Shift+click will select the object under the mouse cursor as the active object.
For tall rooves, I suggest using pixel_z to offset them by a couple of tiles on roof objects.
And if you have placed a bunch of roof objects in a building, but want to see inside the building to edit it, leave this line in your code somewhere:
Simply uncomment the HIDE_ROOVES definition and recompile. Switch over to your map, and the roof objects will all be transparent enough that you can work on the inside of the building without problems. Remember to comment the line out again when you are done, otherwise the alpha is permanent for roof objects at runtime.