ID:2145482
 
This is how BYOND's input handling works by default:





/client/Mouse[Action]() is called, which calls /atom/Mouse[Action]()


Oftentimes in BYOND games, different types of mobs have entirely different means of interacting with the world.

Let's say we have a team-based MOBA that we are working on. Active players will interact with the world differently than observer players. Let's say we want the player to shoot when they click on something, and we want the observer to change the player their camera is following when they click on something. There are several ways to achieve this, all of which are valid.

Method #1: Check player type

client
Click(atom/object,atom/location,control,params)
if(istype(mob,/mob/player))
mob:Shoot()
..()
mob
Click(location,control,params)
if(istype(usr,/mob/observer))
usr.client.eye = src
observer
player


This method is one that I would consider to be pretty bad. It works, and it is incredibly simple, but it can't be easily expanded to encompass many different types of players.

Method #2: polymorphic capability flags

Another method is to take advantage of polymorphism and define flags representing what behavior something is capable of.

client
Click(atom/object,atom/location,control,params)
if(usr.canshoot) usr.Shoot()
..()

mob
var
canshoot = 0
isobserver = 0
proc
Shoot()
Click(location,control,params)
if(usr.isobserver) usr.client.eye = src
observer
isobserver = 1
player
canshoot = 1


This is marginally faster than the first example, and a little bit easier to expand, but we still suffer from a major downfall in that this creates a large number of bloated variables in the root /mob type. This will increase your memory footprint and slow down your game.

Method #3: polymorphic execution chain

There are a number of ways to implement a polymorphic execution chain. What this method does is define a set of hooks on the mob that can be overridden to change the behavior of inputs drastically from one type of mob to another. Let's look at one example in particular:

client
Click(atom/object,atom/location,control,params)
usr.onClick(object,location,control,params)
..()

mob
proc
onClick(atom/object,atom/location,control,params)
observer
onClick(atom/object,atom/location,control,params)
if(istype(object,/mob))
client.eye = object
player
proc
Shoot()
onClick(atom/object,atom/location,control,params)
Shoot()


This method is extremely flexible and allows you reciprocal functions in the form of mob/onClick() and atom/onClick(). There are some speed costs to this approach, but this form of input will start making more sense once we start talking about modality a bit further on down the line. The primary advantage of this approach is that it sacrifices speed for code segmentation for easier debugging and readability so long as you understand the callchain.

Roughly, our execution chain now begins to look like this:




Introducing modality:

Modality is a concept that means that there are multiple ways, or modes that something expresses itself. In our case, we want modality to be modular in that we can change from one input mode to another on the fly and completely change what buttons and keys do with a few keystrokes. This is powerful and will save you tons of spaghetti later on as your game grows.

Method #1: if-statement soup

mob
var/tmp
input_mode
menu/menu
player
onKeyPress(bind)
switch(input_mode)
if(null)
switch(bind)
if("MoveNorth")
client.North()
if("MoveSouth")
client.South()
if("MoveEast")
client.East()
if("MoveWest")
client.West()
if("Accept")
client.Interact()
if("menu")
switch(bind)
if("MoveNorth")
menu.Up()
if("MoveSouth")
menu.Down()
if("MoveEast")
menu.Right()
if("MoveWest")
menu.Left()
if("Accept")
menu.Select()
if("Cancel")
menu.Back()

menu
proc
Up()
Down()
Left()
Right()
Select()
Back()


This approach isn't alltogether a bad one, but it's got an awful lot going on in that onKeyPress function. Every time you create a new mode, you wind up creating a fairly long bit of code in an already long function. We can improve on this by delegating the onKeyPress function to an abstract prototype for handling keypresses. This will disperse the code into the individual menu objects. Let's name an object that consumes the player's input an /interface datum.


Method #2: Modal delegates

client
Click(atom/object,atom/location,control,params)
. = usr.onClick(object,location,control,params)||..()

mob
var/tmp
interface/focus

onClick(atom/object,atom/location,control,params)
if(focus) . = focus.Click(object,location,control,params)

interface
proc
Click(object,location,control,params)


Now that we've implemented this, we can fully change the player's input completely by introducing a handcrafted /interface datum into the mix and setting the mob's focus to that interface. We can even prevent mouse actions from registering at the /atom level by returning 1 to consume the input. If /interface/Click() returns 1, you can bail out of /mob/onClick() without doing the default action. This means that if you only want to change one or two inputs using a modal interface, you can do so while leaving the default /mob actions in place. If /mob/onClick() returns 1, client/Click() will not call /atom/Click() for the clicked object. This means you can prevent individual atoms from responding to events at either the /mob prototype level, or the /interface prototype level. This is a nifty little concept I stole from Javascript.

Now this is what our execution chain looks like (roughly):




Sometimes Code grouping is good for readability and flexibility

I decided later that sometimes I wanted individual hudobjs to handle inputs differently based on what interface they are a part of. I wrote a system for handling many hudobjs as part of a single /interface datum, and each hudobj was given a string-based id. Sometimes, generic hudobjs would have behaviors that were defined under /hudobj/Click(), and other times, the behaviors were delegated to the /interface datum as a separate hook. Essentially, there are downsides to both approaches, but with both available, I was able to get the best of both worlds when the downsides of one approach became too cumbersome.

I redefined the callchain a little bit like so:

client
Click(atom/object,atom/location,control,params)
return usr.onClick(object,location,control,params)||..()

mob
var/tmp
interface/focus

onClick(atom/object,atom/location,control,params)
return focus&&focus.Click(object,location,control,params)

interface
proc
Click(object,location,control,params)
onClick(object,location,control,params)

hudobj
parent_type = /obj
var
id
tmp/interface/owner

Click(location,control,params)
return owner&&owner.onClick(src,location,control,params)


The new execution chain winds up looking something like this:




Now, some of you may be reading this and asking why I went around my elbow to get to my nose like this. I did this so that I had many, many options of implementation based on the specific system I wanted to implement, while also being able to have the minimal CPU impact that I could based on the needs of the implementation. Yes, this longer callchain will result in more proc call overhead, and will not automatically make your code more elegant. However, it will result in fewer actual instructions being processed for very complex input control schemes, and allows individual components of the game to hijack control of parts of the input without having to be strung into the game's core code or split off any more behavior than I already have. Using this approach, I've managed to implement a wide array of interfaces using a minimum of code, complex and bugfree modal input schemes, and all of it uses a methodology that inherently reduces the number of lines of code I wind up having to write and the number of variables I have to define on root-level objects.

As for the speed hit, calling Move() is several orders of magnitude higher than this expanded callchain, so CPU concerns are pretty much a non-issue.
A good way to reduce call overhead from this approach is using capability flags and the event consumption technique we learned earlier to prevent downstream calls from being made on objects that don't need them. While this slows down a full execution chain, it's completely insignificant and speeds up an incomplete execution chain by quite a lot.

#define MOUSE_CLICK         1
#define MOUSE_DBLCLICK 2
#define MOUSE_HOVER 4
#define MOUSE_HOLD 8
#define MOUSE_DRAG 16
#define MOUSE_DROP 32
#define MOUSE_MOVE 64
#define MOUSE_WHEEL 128
#define KEYBOARD 256

#define INPUT_ALL 511

interface
var
control_flags
override_flags

hudobj
var
control_flags

Click(atom/location,control,params)
return container&&container.control_flags&control_flags&MOUSE_CLICK&&container.onMouseClick(src,location,control,params)

atom
var
interact_flags

client
var
control_flags
interact_flags
Click(atom/object,atom/location,control,params)
return mob.control_flags&control_flags&MOUSE_CLICK&&mob.onClick(object,location,control,params)||interact_flags&object.interact_flags&MOUSE_CLICK&&object.Click(location,control,params)

mob
var
control_flags
onClick()
return focus&&focus.override_flags&MOUSE_CLICK&&focus.Click(object,location,control,params)
Spacing out hudobj creation can be important to improving the overall speed the game runs. Especially if you are going to have players flitting in and out or the world reboots regularly with clients still connected. If you have lots of players logging in to a fairly complex game, you probably don't want hundreds of objects per player being created all at one time for dozens or more players.

Spacing out your ui element creation can be done quite easily if you use a modular interface system.

interface
var
showing = 0
initialized = 0
list/screen
destroy_delay = 0
proc
Show()
if(!showing)
if(!initialized)
Init()
showing = 1
if(screen) client.screen += screen
return 1
return 0

Hide()
if(showing)
showing = 0
if(screen) client.screen -= screen
return 1
return 0

hidesleep(duration) //used to delay an action only as long as the interface is hiding. If the interface is shown during the sleep, hidesleep returns false.
var/etime = world.time+duration)
while(!showing&&world.time<etime)
sleep(TICK_LAG)
return !showing

showsleep(duration) //used to delay an action only as long as the interface is showing. If the interface is hidden during the sleep, showsleep returns false.
var/etime = world.time+duration)
while(showing&&world.time<etime)
sleep(TICK_LAG)
return showing

Init()

Destroy()
set waitfor = 0
Hide()
if(!destroy_delay||hidesleep(destroy_delay))
initialized = 0
for(var/hudobj/h in screen)
h.container = null


The above method creates for functions, Show(), Hide(), Init() and Destroy(). Show() returns 1 if successful and 0 if failed. A Show() will only fail if the interface is already showing by default. Hide() is similar, but fails if the interface is not already showing.

Init() is a special abstract function that is called only the first time that an interface is created.

Destroy() is called to destroy all attached hudobjs. This should be overridden to destroy any objects you created in your Init() override. For any uis that are only required once in a little while, I'd recommend destroying them after they haven't been used for a period of several minutes. That way, they aren't just hanging around taking up memory and CPU unless they are needed. Some uis the player will toggle all the time should probably never be destroyed.
Modular components are a way to reduce the amount of code you have to use to create a ui object. This is an example of a modular component and how it is created using the existing structure:

interface
proc
onButton(atom/object,atom/location,control,params)
return 0

hudobj/button
icon_state = "1"

proc
Disable()
icon_state = "0"

Enable()
icon_state = "1"

MouseEntered(location,control,params)
if(container&&container.showing)
if(icon_state!="0")
icon_state = "2"
return 1
return 0

MouseExited(location,control,params)
if(icon_state!="0")
icon_state = "2"

MouseDown(location,control,params)
if(container&&container.showing)
if(icon_state!="0")
icon_state = "3"

MouseUp(location,control,params)
if(container&&container.showing)
if(icon_state=="3")
icon_state = "2"
return container.onButton(src,location,control,params)
else if(icon_state=="1")
icon_state = "2"
else if(icon_state!="0")
icon_state = "1"

MouseDrop(over_object,src_location,over_location,src_control,over_control,params)
if(icon_state!="0")
if(over_object==src)
icon_state = "2"
else
icon_state = "1"

New(interface/owner,id=null,icon=null)
if(!src.icon) src.icon = icon
container = owner
src.id = id


Here's the format for our buttons' icons so all we have to do is create a new DMI for each button using the following format.



State 0 is disabled.
State 1 is enabled.
State 2 is hovered.
State 3 is pressed.

Now let's say you have an interface with a bunch of buttons that you can press:

interface/example
Init()
screen = list(new/hudobj/button(src,"btn1",'btn1.dmi'),new/hudobj/button(src,"btn2",'btn2.dmi'),new/hudobj/button(src,"btn3",'btn3.dmi'))

onButton(hudobj/object,atom/location,control,params)
switch(object.id)
if("btn1")
//do something
if("btn2")
//do something
if("btn3")
//do something
else
return 0
return 1


Now each interface can have a huge variety of buttons that do different things per interface without having to define a whole bunch of button objects. It's the interface that responds differently, not the button. All buttons behave the same way. It's the interface's code that varies.

A wide array of modular objects can be created this way. The sky and your dedication is the limit.
Abstract Input bindings:

macros are OUT. No. Seriously. They are completely out. Stop it. Don't bother with any more than two since BYOND 511.



Congratulations. Close the skin editor. You are done.


Let's look at some of the code for handling these guys:

interface/proc
Focus(mob/m,interface/ofocus)

Unfocus(mob/m,interface/nfocus

#define ACCURATE_TIME (world.tick_usage/100*world.tick_lag + world.time)

#define KEYBINDS 4 //how many actions are bound to keys. several different keys can be bound to the same actions. Only count the numeric binds, not the keys bound to numeric binds.
#define TAP_THRESHOLD 2.5 //how long in ticks before a keystroke is no longer a multitap

#define bind2dir(b) __dirs[b]
#define bind2opp(b) __invdirs[b]
#define dir2opp(d) __oppdirs[d]

var/list/__dirs = list(NORTH,SOUTH,EAST,WEST)
var/list/__invdirs = list(SOUTH,NORTH,WEST,EAST)
var/list/__oppdirs = list(SOUTH,NORTH,NORTH|SOUTH,WEST,SOUTHWEST,NORTHWEST,NORTHWEST|SOUTH,EAST,SOUTHEAST,NORTHEAST,NORTHEAST|SOUTH,EAST|WEST,SOUTHEAST|WEST,NORTHEAST|WEST,NORTHEAST|SOUTHWEST)


client
var
list/key_binds = list("W"=1,"S"=2,"D"=3,"A"=4) //stores a list of keybinds associated to the numeric bind id
//these lists uses numeric bind ids from key_binds, not key associations as indexes
list/key_state[KEYBINDS] //stores the current number of buttons pressed bound to this bind
list/key_taps[KEYBINDS] //stores the number of taps in sequence for each bind
list/key_time[KEYBINDS] //stores the time of the last press/release for each bind
last_key = null //stores the last key pressed or released in string format.
verb
//called when you press a key
keyPress(key as text) //key is supplied in text form by the [[*]] arg
set instant = 1 //make sure the event can happen more than once per tick
set hidden = 1 //don't show the verb anywhere
var/bind = key_binds[key] //look up the action by key
if(bind) //if the key is bound to an action
var/olk = last_key //store the old last key
last_key = key //store this key as the new last key
if(++key_state[bind]==1) //if bind wasn't in use but now is
var/otime = key_time[bind], ntime = ACCURATE_TIME, tdelta = ntime-otime, taps = key_taps[bind]
//set up the input parameters
if(olk==key&&tdelta<=TAP_THRESHOLD) //if this is a double-tap
taps++
else //otherwise, reset
taps = 1
key_time[bind] = ntime //set the time of this keybind's invocation
key_taps[bind] = taps //store the tap state for this bind
mob.onKeyPress(bind,taps,ntime,tdelta) //delegate to the mob

//called when you release a pressed key
keyRelease(key as text) //key is supplied in text form by the [[*]] arg
set instant = 1 //make sure the event can happen more than once per tick
set hidden = 1 //don't show the verb anywhere
var/bind = key_binds[key] //look up the action by key
if(bind) //if the key is bound to an action
var/ks = --key_state[bind] //decrease the number of times this bind is being held down (multiple keys can be bound to the same action)
var/olk = last_key //track the previous last key
last_key = key //store the current bind as the last key
if(!ks) //if the key was 1 but is now 0
var/otime = key_time[bind], ntime = ACCURATE_TIME, tdelta = ntime-otime, taps = key_taps[bind]
if(olk!=key||tdelta>TAP_THRESHOLD) //reset the tap count if the key doesn't match the last key or the time between press and release is too great
key_taps[bind] = 1
key_time[bind] = ntime //store the current time the key was last released
mob.onKeyRelease(bind,taps,ntime,tdelta) //delegate to the mob event
else if(ks<0) //if the key was already zero, but has gone negative, reset to 0.
key_state[bind] = 0

North() //get rid of this crap.
South()
East()
West()
Northeast()
Northwest()
Southeast()
Southwest()
Center()


And here's an example move handler:

mob
var/tmp
move_keys
move_dir
proc
onKeyPress(bind,taps,time,delta)
if(!focus||!(focus.override_flags&INTERACT_KEYBOARD)||!focus.keyPress(bind,taps,time,delta))
switch(bind)
if(1 to 4)
var/d = bind2dir(bind)
var/opp = bind2opp(bind)
move_keys |= d
move_dir |= d
if(move_keys&opp)
move_dir &= ~opp

onKeyRelease(bind,taps,time,delta)
if(!focus||!(focus.override_flagss&KEYBOARD)||!focus.keyRelease(bind,taps,time,delta))
switch(bind)
if(1 to 4)
var/d = bind2dir(bind)
var/opp = bind2opp(bind)
move_keys &= ~d
move_dir &= ~d
if(move_keys&opp)
move_dir |= opp

//call to change the control focus to another interface
setFocus(interface/nfocus)
var/interface/ofocus = focus
if(ofocus)
ofocus.Unfocus(src,nfocus)
else if(nfocus)
Unfocus(nfocus)
focus = nfocus
if(nfocus)
nfocus.Focus(src,ofocus)
else if(ofocus)
Focus(ofocus)

Focus(interface/ofocus)
var/list/ks = client.key_state
var/list/kt = client.key_time
var/mk
var/md
if(ks[1])
if(ks[2])
mk += 3
if(kt[2]>kt[1])
md += 2
else
md += 1
else
mk += 1
md += 1
else if(ks[2])
mk += 2
md += 2
if(ks[3])
if(ks[4])
mk += 12
if(kt[4]>kt[3])
md += 8
else
md += 4
else
mk += 4
md += 4
else if(ks[4])
mk += 8
md += 8
move_keys = mk
move_dir = md

Unfocus(interface/nfocus)
move_keys = 0
move_dir = 0

MoveLoop()
set waitfor = 0
while(client)
if(loc&&move_dir)
step(src,move_dir)
sleep(world.tick_lag)
Login()
. = ..()
MoveLoop()


A few pointers here. Unfocus/Focus are used to reset the move_keys and move_dir variables of the mob. When control focus shifts away from the mob, you don't want movement to carry on and you should rebuild the keys to handle repeating behavior in your elements. This will allow you to ignore the repeating of a macro arbitrarily simply by changing a variable or calling a proc, rather than constantly checking every time it is held down.

One particularly cool thing about this, is that you can easily dynamically rebind keys and store them on the client or server side without using rubbish winset() calls.
This post helped me understand the Any macro and its significance. I'll probably redo my controller system to incorporate, but I do have a question before moving forward: what IDs do macros inherit when using the ANY macro? For example, if I wanted to attach the "attack" command to the A key, I might give it an ID of "key-down-attack".

Depending on what the answer to the above is determines whether I'll go the extra lengths to redo my controller system. (This post did give me some ideas on how to refactor other areas of my project though.)

Thanks for sharing.
In response to FKI
If you want to attach a specific verb to a specific key, then just do that. You don't really need the "Any" macro if you already know what keys go to what command.

Where the "Any" macro shines is in dynamic key handling, to map any variable button to any procs, or to enable a rough implementation of determining whether any given key is currently down or up, without having to assign separate macros to every possible key.