ID:1797736
 
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)
Sorry I spent so long hacking this snippet together. Some things went wrong and I discovered a few BYOND bugs along the way.

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.

mob/verb/test_appearances()
var/obj/o = new()
o.icon = 'someicon.dmi'
src.overlays += o
world << "Appearance 1: \ref[src.overlays[src.overlays.len]]"
world << "Overlays 1: \ref[src.overlays]"
o.icon = 'someicon2.dmi'
src.overlays += o
world << "Appearance 2: \ref[src.overlays[src.overlays.len]]"
world << "Overlays 2: \ref[src.overlays]"
overlays.Cut(src.overlays.len-1,0)


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.

area
var
area/root_area
list/subareas
proc
//calls subdivide on the root area.
//creates a new child area and links it with this one as the root
Subdivide()
//if there is a root area set, we need to perform the root's subdivide operation.
if(root_area)
return root_area.Subdivide()
else
//create a new area instance of the same type as this area
var/area/a = new src.type()
//set its root to this area
a.root_area = src
//if we don't already have a subarea list, create it and add the new area, otherwise, just add the new area
if(!subareas)
subareas = list(a)
else
subareas += a
return a

//returns the contents of both the root area and all of the subareas.
getContents()
if(root_area)
return root_area.getContents()
var/list/l = src.contents.Copy()
if(subareas)
for(var/area/a in subareas)
l.Add(a.contents)
return l

//returns 1 if the supplied area is a child or sibling subarea to this one, or is src
//returns 0 if the supplied area is not a matching area.
isSame(area/a)
if(a.type!=src.type) return 0
if(a==src) return 1
if(a.root_area==src||src.root_area==a) return 1
return 0

Enter(atom/movable/O, atom/oldloc)
//only call Enter on the root area
if(root_area)
return root_area.Enter(O,oldloc)
return ..()

Exit(atom/movable/O, atom/newloc)
//only call exit onthe root area
if(root_area)
return root_area.Enter(O,newloc)
return ..()

Entered(atom/movable/O, atom/oldloc)
//only call entered on the root area
if(root_area)
return root_area.Entered(O,oldloc)
//this will only be run if this area is the root area
if(oldloc)
//if the movable is moving here from elsewhere
//get the old turf the player was associated with
var/turf/t = locate(oldloc.x,oldloc.y,oldloc.z)
//if the turf exists
if(t && isSame(t.loc))
//if the old area's root area is this object, don't trigger Entered() behavior.
//return 0 even though it's not necessary. We can use this for expanded behavior later.
return 0
//provided the movable is coming from an area that's not a subdivision of this one:
return ..()

Exited(atom/movable/O, atom/newloc)
//only call exited on the root area
if(root_area)
return root_area.Exited(O,newloc)
//this will only be run if this area is the root area
if(newloc)
//if the movable is leaving to another location
//get the new turf the player will be moving toward
var/turf/t = locate(newloc.x,newloc.y,newloc.z)
//if the turf exists
if(t && isSame(t.loc))
//if the new area's root area is this object, don't trigger Exited() behavior.
//return 0 even though it's not necessary. We can use this for expanded behavior later.
return 0
//provided the movable is going to an area that's not a subdivision of this one:
return ..()


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().

#define ROOF_LAYER 7

var
initialized = FALSE
//these are temporary lists. They should be null after world initialization
list/__building_rooves = list()

roof
parent_type = /obj
layer = ROOF_LAYER

New()
//roof objects should only be used on the map
if(isturf(src.loc))
//roof objects should not be created at runtime
if(!initialized)
//get the base roof object from the building list
var/roof/base_roof = __building_rooves[src.loc]
if(base_roof)
//if the base roof object exists, add this roof's appearance to its overlays and set location to null, triggering garbage collection
base_roof.overlays += src
src.loc = null
else
//if the base roof object doesn't exist, add this object's appearance to its own overlays then store it in the building list by location
overlays += src
__building_rooves[src.loc] = src
else
//if it's after the map has been initialized, just set this object to location null to trigger garbage collection
src.loc = null
else
//if we're not in a turf, set our loc to null, hopefully triggering garbage collection.
src.loc = null


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.

world
New()
..()
//construct the rooves after the map loads
construct_rooves()
//set initialized to true to indicate that the map has already finished loading
initialized = TRUE

var
list/__unique_rooves = list()

proc
construct_rooves()
var/roof/roof
var/area/area
var/area/subarea
var/refid
var/list/subtypes
//loop through all the objects in the temporary building rooves list
for(var/turf/turf in __building_rooves)
//get the roof object reference from the base turf
roof = __building_rooves[turf]
//get the current area the roof object is in
area = turf.loc
//get the overlay list reference of the roof object
refid = "\ref[roof.overlays]"
//the unique rooves list stores a temporary association of areas with their particular unique roof overlays
subtypes = __unique_rooves[area]
//if this area doesn't have a subdivision yet
if(!subtypes)
//create the subdivision list and fill it with the current unique overlay reference
subarea = area.Subdivide()
subarea.overlays = roof.overlays
//set up the storage list
subtypes = list()
subtypes[refid] = subarea
__unique_rooves[area] = subtypes
else
//otherwise, make certain that this unique overlay list isn't in use yet.
subarea = subtypes[refid]
if(!subarea)
//if it's not, we create a new area subdivision
subarea = area.Subdivide()
subarea.overlays = roof.overlays
subtypes[refid] = subarea
subarea.contents += turf
//remember to trigger garbage collection for all roof objects, removing them from the world
roof.loc = null
//empty the temporary lists as they are no longer needed.
__unique_rooves = null
__building_rooves = null


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.

#define ROOF_FADE_TIME 10 //how long the fade animation should take

proc
//round to the next whole number to the right
ceil(num)
. = round(num)
if(.<num)
.++

client
var
list/hide_rooves = list()

roof_hider
var/tmp
client/owner
area/area
list/images
fade_time
proc
Hide()
if(!images)
//if the images list doesn't exist, create images for each subarea
var/image/i
images = list()
for(var/area/a in area.subareas)
//create a blank image
i = image(null,a)
i.overlays = a.overlays
//make them non-interactable to the mouse
i.mouse_opacity = 0
//make the image hide the default appearance of their subarea
i.override = 1
//add the image to the images list
images += i
var/stime = world.time + world.tick_lag
fade_time = stime
owner.images.Add(images)
//add the images to the client's images list to show them
owner.hide_rooves[area] = src
spawn(world.tick_lag)
if(fade_time==stime)
for(var/image/j in images)
animate(j,alpha=0,time=ROOF_FADE_TIME)
else
//if the image list exists, we need to fade in based on the current
//alpha of the images in the list
var/oldtime = max(min(world.time - fade_time,ROOF_FADE_TIME),0)
var/startalpha = 0 + 255*oldtime/ROOF_FADE_TIME
var/duration = startalpha/255*ROOF_FADE_TIME
fade_time = world.time
//perform the fade animation
for(var/image/j in images)
//stop the old animation
j.alpha = startalpha
animate(j,alpha=0,time=duration)
Show()
//fade out based on the current alpha of the images in the list
var/oldtime = max(min(world.time - fade_time,ROOF_FADE_TIME),0)
var/startalpha = 255 - 255*oldtime/ROOF_FADE_TIME
var/duration = (255-startalpha)/255*ROOF_FADE_TIME
//if the duration is longer than one tick, perform the animation
if(duration>=world.tick_lag)
//loop through the images and perform the fade
var/stime = world.time+world.tick_lag
fade_time = stime
for(var/image/j in images)
animate(j)
j.alpha = startalpha
spawn(world.tick_lag)
for(var/image/j in images)
animate(j,alpha=255,time=duration)
//make sure the fade animation is done
spawn(ceil(duration))
//if the fade animation finished, remove all the images
if(fade_time==stime)
owner.images.Remove(images)
owner.hide_rooves.Remove(area)
//set the images' locs to null to make sure they get garbage collected
for(var/image/j in images)
j.loc = null
else
//otherwise, we just straight up remove all of the images
owner.images.Remove(images)
owner.hide_rooves.Remove(area)
//set the images' locs to null to make sure they get garbage collected
for(var/image/j in images)
j.loc = null

New(Area,Owner)
area = Area
owner = Owner


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:

area
var
area/root_area
list/subareas
proc
HideRoof(client/client)
if(subareas)
//create a new hider object if one doesn't already exist for this area on the client
var/roof_hider/hider = client.hide_rooves[src]
if(!hider)
hider = new(src,client)
client.hide_rooves[src] = hider
//call the hide routine
hider.Hide()

ShowRoof(client/client)
if(subareas)
//get the existing hider object
var/roof_hider/hider = client.hide_rooves[src]
if(hider)
//call the show routine
hider.Show()
Entered(atom/movable/O, atom/oldloc)
if(root_area)
return root_area.Entered(O,oldloc)
//this will only be run if this area is the root area
if(oldloc)
//if the movable is moving here from elsewhere
//get the old turf the player was associated with
var/turf/t = locate(oldloc.x,oldloc.y,oldloc.z)
//if the turf exists
if(t && isSame(t.loc))
//if the old area's root area is this object, don't trigger Entered() behavior.
//return 0 even though it's not necessary. We can use this for expanded behavior later.
return 0
//provided the movable is coming from an area that's not a subdivision of this one:
if(istype(O,/mob))
var/mob/m = O
if(m.client)
HideRoof(m.client)
return ..()

Exited(atom/movable/O, atom/newloc)
if(root_area)
return root_area.Exited(O,newloc)
//this will only be run if this area is the root area
if(newloc)
//if the movable is leaving to another location
//get the new turf the player will be moving toward
var/turf/t = locate(newloc.x,newloc.y,newloc.z)
//if the turf exists
if(t && isSame(t.loc))
//if the new area's root area is this object, don't trigger Exited() behavior.
//return 0 even though it's not necessary. We can use this for expanded behavior later.
return 0
//provided the movable is going to an area that's not a subdivision of this one:
if(istype(O,/mob))
var/mob/m = O
if(m.client)
ShowRoof(m.client)
return ..()


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:

//#define HIDE_ROOVES 1

roof
layer = ROOF_LAYER
#ifdef HIDE_ROOVES
alpha = 96
#endif


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.
Here's an overly long and boring video that shows you how to use the setup to map out your rooves and buildings.



Or, if you'd just like to take a look for yourself:

Demo

Library
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)
Okay, everything is finished. Video, Library and Demo are up in the third post.
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.
Don't worry Lummox, I'll hit up the bug reports forum shortly.
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.
The name is part of the Appearance, so it should override.
I've been reading all of your Snippet Sunday's to pass the time as of late and I'm consistently impressed. Cheers to another good read, Ter13.