I thought it was time to revisit many of my snippet sundays articles with additional information from the last six or so years since I started the series.
Lummox has been a huge doll and improved a lot of what the engine is capable of in a variety of areas.
Many of these changes are very low-level and abstract, which is fantastic for people who can figure out how to apply them in a variety of ways. However, because they are so abstract, you may not immediately realize just how widely useful they can be.
Each post below is concerned with an addendum to my existing snippets, one snippet per post.
ID:2544923
Feb 15 2020, 5:38 pm (Edited on Feb 15 2020, 10:02 pm)
|
|
Snippet #2 revisit
My second snippet was about using the map editor. Very little on this front has really changed. Most of the advice is still sound, however Lummox added a quality of life change to "generate from icon-states/directions". It no longer changes the tag of objects generated, thus making it actually useful now. You can generate instances from icon_states much faster now. I also bring along a few more tips to share with the class that I find myself using a lot these days: Never lose your prototypes: If you need to generate a bunch of prototypes, digging through the terrible instance editor menu can be really confusing and steal a lot of your time. Here's a neat way to speed up your workflow: Generate your prototypes in an empty DMM: This way, you can safely delete stuff from the map without having to regenerate prototypes lost during compilation. Don't include the palette DMM file by unchecking the box in the file file tree. When you are manually generating atlases to take into your map with you, you can uninclude all other maps and recompile the project. (If your project takes a long time to compile, you can use a project with much less going on code-wise to generate your atlas DMMs.) Text edit: DMMs are human readable. You can edit them in a text editor. Right click on your map, and open with notepad. You can modify tile pops and instances manually here. If you format your icon files in a regular way, you can actually speed up the generation of prototypes quite a lot using this method. For instance, I have a tool that will allow you to generate 47 unique tile blending states from a 3x4 tile blank like this one: It generates an icon state structure like this: These would be a pain to import, and put in a little atlas so you can pick the right join states to pretty up your map a little bit. (The same tool also allows me to merge the map at world start automatically, but sometimes I choose to not use it and manually map join states where it's not too much of a pain to do so.) If you have a consistent icon state structure, you can quickly import all of the prototypes with a simple process. First, we map out the first atlas manually by creating all possible states and arranging them how we want all other atlases to look: Then, we open the project folder: We find our dmm file in the project folder (make sure you saved it.) and then we make a copy. We then open the copy with Notepad: Next, we use Find/Replace... in the edit menu of notepad to change all instances of the old icon file with the new icon file. We can even add different properties to each prototype here, like changing the layer, or the density if we want. We save the file as a new DMM, and then we do the same thing over again for as many autotiles we want to import. Open up the new DMM files in Dream Maker (refresh on the file tree to show the new dmms), and you can copy/paste the new prototypes as atlases into a new map. You don't have to include the prototype atlas dmm files in your project to use them. Just copy the tiles you need into a map you actually plan to include, and your workflow will be a ton faster overall. As long as the structure of your icon files is consistent, you can severely speed up the importing of prototypes. Develop a system for generating content, and you can massively boost your productivity. |
Snippet #3 revisit
My third snippet was about creating databases of modified types rather than using polymorphism to pull it off. It's really missing something, though. The example structure I showed off is a pretty basic implementation of the idea. BYOND adding JSON into the mix makes building databases of variations of items and using singleton objects to define the behavior of quantitative representations of those singletons a lot easier. Let's take a look at creating an object instance using json: proc/json2datum(json,default_type=/datum) This will allow us to quickly create an object dynamically from a JSON-sourced associative list. You can load these files at runtime to populate your databases with instances. |
Snippet #6 revisit
Your code still sucks for the same reasons it always has. BYONG has gotten a little faster in some places, and a little slower in others (but the places it has gotten slower is because of new features that made stuff that was impossible before now possible, so fair trade.) I suppose I should have also included some information in this one about diagnosing CPU issues, and identifying "jitter", "lag", "chop", etc. So let's talk about these terms so we can correctly determine where our problems are and intelligently try to chase them down. 1) CPU overrun. When people say BYOND is laggy, this is often what they mean. They don't mean that it's just slow over a network. That's more of a problem with the connection than BYOND itself. They mean that it can't handle all the shit code that they have stacked on top of it. What's happening here, is that there's too much for the server to keep up with happening every frame. This is usually just down to bad code. But we can diagnose this problem using the profiler. The code profiler will tell us how much of the CPU budget any one thing is using. Self CPU is how much time a proc actually spends processing its own instructions. Total CPU is how much time it takes for a proc to complete from start to finish. This is actually time spent occupying the scheduler, not real time. So any time that a proc is paused by a sleep(), for instance, will not contribute to this stat. Real Time is how much time it takes for a proc to complete from start to finish, including waiting. Overrun is how long a proc spends processing when the interpreter is over its CPU allowance. This right here is the source of unresponsiveness, as when the server goes into overrun, everyone misses ticks and the whole game bogs down. Just because a proc happens in overrun, though, doesn't mean it's the cause of the overrun. These values are all totals, so to figure out how good or bad these are, you also need to know how much time has been allotted to the profiler to collect this data. This will help you get a relative picture of how long your game is spending doing certain things, and where optimization can be useful. If you have a proc that is called tens of thousands of times, shaving off one one thousandth of a second from how fast it goes can save you more CPU budget than shaving a full second off a proc that only runs once. How often, and when a proc runs is important to understanding what you should be tackling in terms of speed gains and losses. If your game is becoming unresponsive, and all these numbers look good, there's a good chance that your game is having problems elsewhere. Let's look at where that could be: Client rendering can't keep up We have a limited amount of client information available to us. The client profiler will tell you how much time is spent on each step of rendering. In general, the more objects in the view, screen, images, statpanels, and grids of the client, the longer rendering takes. You can tell if the client is the thing that's bogging down by looking at the task manager and finding dream seeker. If it's regularly hitting 100% of its cpu budget (if you have multiple cores, 100% might be less than 100%), or memory is just climbing out of control, that would be a good indicator that you've done something wrong with regard to how much information you are sending clients. Jitter/Chop You might not actually be seeing "lag". You might be seeing jitters caused by badly misconfigured glide sizes, or from skin elements overlapping the map, causing render choppiness due to windows being an asshole. Real lag If all else fails, your network might just not be receiving frames very well due to packet loss or latency spikes, and now you can actually complain about lag, because this is actual lag. None of the other above categories are lag, and they are all fixable. This one might be, but probably won't be and is out of the scope of this guide. |
Snippet #9 revisit
Snippet #9 dealt with getting some useful info out of the client for properly managing map resize events. BYOND 513 added a new feature that makes this more responsive and easier to manage. We can now include winget queries inside of skin element command fields. These will automatically pass any property you want as an argument of a verb that's called by an on-resize, on-show, on-hide, on-focus, on-blur, or on-command field of any relevant skin element. The format of these embedded wingets is: "[[property]]", or you can get the property of another element: "[[id.property]]", or you can get the property of the parent element: "[[parent.property]]". This allows us to phase out a lot of the extra communication that our implementation of a dynamic view size required. Let's revisit how we would do this: First, we want to pass the id of the map, and the size of the map with the verb that is triggered when the main map resizes. This allows us to simplify our code a little bit. #define TILE_WIDTH 32 We've eliminated the server-side winget(), speeding up the on-resize event. Let's also take a look at another new BYOND feature: screen_loc now supports map-relative anchors. This makes handling dynamically resized map bounds a lot easier to deal with. These new anchors are: TOP, BOTTOM, RIGHT, and LEFT. In addition, screen objects support percentage-based anchors similar to how skin elements anchor their components. When the viewport is smaller than the map element, NORTH, SOUTH, EAST, and WEST will always be the same as TOP, BOTTOM, RIGHT, and LEFT, but when the bounds of the viewport exceed the edges of the map element, as our system always tries to do, the map-edge screen anchors will move objects to the edge of the map element, preventing the overlap problem that this snippet was designed to counter. Our code is still useful, but we no longer need to actually update screen elements' screen locations using buffer_x/buffer_y after the resize. BYOND does it for us now if we set up our screen_locs properly now. |
Snippet #11 revisit
Snippet #11 was concerned with operators. A few changes to operators have happened. ?./?: safe access operator: If an object reference is null, you'll get a runtime error if you try to access one of its variables. This leads you to write code like this all the time: if(usr && usr.name=="herpderp") Safe access operators allow you to do this in a single pass: if(usr?.name=="herpderp") The conditional access operator will return null if the reference is null, preventing the runtime error from accessing the properties of nothing. ?: is the same thing, but doesn't perform compiler safeguards on what you are trying to access just like the runtime lookup operator. Binary operator changes: Binary operations used to be limited to 16 bits. They have been expanded to support 24 bits. Invocation changes: DM now supports call chaining. somelist[1]:dothing() The above is valid syntax now. getobject():dothing() The above is valid syntax. You can now call a proc on an object that's been returned by another proc, and you can also call a proc on something grabbed from a list by index or associative key. Equivalence operators: ~= is sort of like ==, except it allows some functionality that regular equality operators don't. ~! is the inverse, or not-equivalent operator. A few rules for this operator: Two lists will be equivalent if their contents are the same. Two matrix datums are equivalent if their values are the same. Two datums are equivalent if A.operator~=() proc returns true with argument B. Operator overloading: You can now overload operators for datums. This allows you to implement your own behavior when using operators on an object. Most operators can be overridden, and it makes developing interfaces that are easy and intuitive to use a lot easier and require less documentation. |
Snippet #12 revisit
BYOND recently allowed access to 24 bits with binary math, not 16 as we covered above, so this only changes a few small things in Snippet #12. |
Snippet #13 revisit
As we covered above in Snippet #1. vis_contents. vis_contents lets us avoid a significant amount of appearance churn due to updating overlays. If you are regularly adding/removing/updating/readding overlays, change that approach to take advantage of vis_contents and watch your code magically get sprightlier. |
Snippet #16 revisit
Snippet #16 was concerned with masking. I found a neat trick to do masking, and recent updates to the engine allow this behavior natively. The new structure: root KEEP_TOGETHER { background VIS_INERIT_ID { } fill BLEND_INSET_OVERLAY VIS_INERIT_ID { } foreground VIS_INERIT_ID { } } The original code changes very little. BLEND_INSET_OVERLAY is the only real change. What this appearance flag does, is mask the child element against the containing KEEP_TOGETHER object. This will prevent any overflow from showing up, and as an added bonus, BYOND will not trigger mouse interactions on any masked portion of the object. The consequences of this change are extremely profound, as it allows you to now create dynamic, nested UI elements with their own content areas: |
The very first Snippet Sunday spent time talking about overlays. Not a lot has really changed on this front since it was written in 2014. Some new features have come out that make overlay cruft a lot easier to avoid in your projects. Let's talk about those.
BYOND 512: vis_contents:
vis_contents are an awful lot like overlays, but the difference is that overlays are immutable, and vis_contents are not. Any updates to an object in vis_contents will show anywhere that the same object is shown in another object's vis_contents. You can create complex hierarchies of objects using vis_contents, as the vis_contents of vis_contents will show on top of the root object anywhere it renders.
Very similar rules to overlays apply in how appearances are rendered in vis_contents, however, a BYOND 513 feature allows you to configure how these objects behave when rendered as vis children of another object: vis_flags.
vis_flags is sort of like appearance_flags, but are only used when the object is a vis_child of another object. Overlays by default inherit the parent object's direction, icon, and icon_state (when null), and using FLOAT_LAYER/FLOAT_PLANE, can inherit the object's layer and plane. vis_flags allows you to specify the same behavior of an object when it is a child of another object:
VIS_INHERIT_ICON
VIS_INHERIT_ICON_STATE
VIS_INHERIT_DIR
VIS_INHERIT_LAYER
VIS_INHERIT_PLANE
These flags all deal with how the object will render as a vis_child without affecting its on-map appearance at all. This allows you to do really cool things like showing a player's icon with live visual updates somewhere on another player's UI, without the icon_state or direction changing in the UI element. Or let's say you have a system where you want to show a 4-directional paper doll of the player during character creation. Normally, you'd have to update 4 objects' appearances with each change to the player's visual appearance, but now you can just set up four root objects and add the player as a vis child of the four directional objects, and any changes you make to the player will automatically be mirrored to the screen objects with almost zero overhead.
VIS_INHERIT_ID is a little more complicated. We can use this to treat vis children as a piece of the parent for the purposes of Mouse interaction. Any mouse interaction with that object where it is rendered as a vis child when this flag is set, will instead call the mouse procs of the parent, with the icon-x/icon-y params being based on the display position of this object. This is very useful for simplifying the creation of dynamic UI widgets out of a series of objects.
VIS_UNDERLAY forces any vis children with this flag set to render underneath the parent, regardless of their layer. vis underlays will interlayer with themselves according to their plane/layer.
VIS_HIDE will cause this object to not appear at all as a vis child of something else. I can't think of a use case for this, but I'm sure someone will find one.
Why is this a big deal?
So much of our time as BYOND developers is spent working around the inherent limitations of the system. Things like making multi-piece UI elements has always been stupidly difficult to pull off, and always had some major gotchas attached to them, like not being able to move them around the screen without looping over potentially dozens of objects, and not being able to specify that an object is inside of a content panel, thus hiding anything that overflows a dynamic masking area.
Vis_contents changes all of this by giving us direct access to a built-in hierarchal rendering model. It also allows us to do completely certifiably insane things like adding a turf to an object and showing anything that's in that turf on top automatically, allowing for really neat systems like map edge wrapping, and linking Z-layers visually.
Any system that you wrote that involved adding and removing overlays over and over again during the lifetime of an object can now be done in a much more straightforward and simplistic manner. Rather than creating tons of appearance churn when adding and removing overlays to/from an object, vis_contents uses a much more flexible and straightforward messaging format, making updating tens of thousands of vis_contents objects not completely shred performance on the server and the client.
Here's some examples of things that can be done with vis_contents:
Easy Dynamic Reflections:
The above example adds a vis child underlay we'll call 'reflection' to the player that inherits icon, icon_state, dir, and layer. We scale this object vertically by -1, then offset it by the player icon's height minus 1. This object has a color matrix applied, and a repeating horizontal scaling animation to give it an undulating effect. It is then set to render on REFLECTION_PLANE (-1). Tiles that show the player's reflection should render on REFLECTION_PLANE, while all other tiles should render above the reflection plane.
This means that reflections will always be shown to the client, but the map will hide them during rendering unless they are in a space that shows the reflections. It's an easy, low-impact, low-code solution to something that without vis_contents, would have infected almost every other aspect of your code. Let's take a look at the code:
UI Widgets:
Notice how the UI element resizes gradually? That's because we can now live-update the components that the object is built out of, including animate()ing them. This allows much more flexibility and leeway in constructing objects, and now you can get much more visual flair out of complex objects.
Adding one of these components to the screen is simple. We create one, and add the root object to the screen. vis_contents brings all the constituent elements right along with it, so it really cleans up our vis_contents code.
Also, if you want to handle clicking on a vis_contents-based compound object, VIS_INHERIT_ID makes it so that you don't have to write MouseDown()/MouseMoved() hooks in all of the child objects and refire those events to the parent. INHERIT_ID will automatically look for the first parent object without this flag set and treat the child where the mouse event happened as the object that was interacted with. This makes gorgeous, intuitive UIs much easier to manage.
There are a ton more possibilities with vis_contents. Start looking at your overlays and start asking yourself if the burden of working with immutable overlays is necessary in your approach. Remember vis_contents. It's changed everything.