Jump to content
  • Advertisement
Sign in to follow this  
cozman

Unity Input System design discussion

This topic is 4841 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

This is more of a discussion than a question, like Toolmaker's recent thread on State management. I have encountered a few problems in my libraries current input system and I'm planning to take the opportunity to overhaul some of the input system and I wanted to get some input. I realized I never put a ton of thought into designing an input system, essentially just writing the bare minimum, but I'd like to get something that is pretty versatile working this time. At the moment I have some general purpose input access functions available:
bool mouseButtonPressed(MouseButton)
bool keyPressed(Key)
uint getMouseX()/getMouseY()
...
I also have an InputListener class:
class InputListener
{
    virtual onKeyPressed(Key);
    virtual onMouseMove(uint x, uint y);
    ...
};
The problem is that InputListeners register themselves with the input system when they are created, and deactivate themselves when they are deleted which has lead to some random crashes when an InputListener removes itself which is a pretty tricky situation to handle. (It seems the trouble stems from one InputListener derived class removes itself when ESC and another callback is called before the Input system can properly remove the now destroyed class) I'm considering adding the virtual InputListener functions to my State class so that my StateManager can delegate events to the active State. Any input would be helpful. If you want to just post what you use for an Input system or what you'd like to use in a library, any input would be good to hear. edit: I found that my idea is similar to the State/Input system proposed in this OGRE wiki entry PS. I'm using GLFW but I prefer to keep it relatively library agnostic.

Share this post


Link to post
Share on other sites
Advertisement
Instead of having every object that listens to input having a listener registered with a central class, instead input is passed down a tree of objects (based on the decision of each node of the tree). In other words, it is a very standard GUI system. The state system is also built as a state tree, so input gets passed along to the active state but can also be intercepted by another state if it is active.

It also keeps the actual input code completely decoupled from the GUI code. The main loop just takes the input out of the input manager and then feeds into the GUI system. So scripted input could easily be added into the system (since it can be fed into the GUI system with no changes to the input system).

Share this post


Link to post
Share on other sites
You want your actual game code to listen for named triggers.

You then want a separate system that listens for physical input (or network, or game recording, or test script, or ...) and maps that to triggers. Here's where user-customizable keyboard mapping happens.

You then want some rules for arbitrating who actually gets to hear what's going on -- for example, if space is both shoot while in-game, and select while in a menu, you don't want it to shoot in the underlying game if you're currently overlaying with a menu.

Share this post


Link to post
Share on other sites
I thought about this a bit, too, when implementing input in my engine. What I decided to do in the end was to have a number of different "actions" (forward, backward, move_up, crouch, etc.) and you could bind those actions to keys. The input system pipes the key-presses to the actions and you can change it the key configuration any time.

There was also another engine that I did that you could overload any class with InputHandler and you'd then register with a management class and that would send input to it. I don't really like this approach, though, because it doesn't leave much room for customization in a clean way.

Share this post


Link to post
Share on other sites
intrest86: Thanks for the idea, I was actually thinking along similar lines. I am pretty sure that it would be best to write it so that only one thing can be the listener, and it is it's responsibility to dispatch messages further.

hplus0603/Mercury: Thanks for bringing that up, that's actually the next step for me, I'm still more on the delegation of actual input events. But when I go to deal with tying events to actions I'll take what you guys said into consideration.

Essentially what I'm thinking now is to go with the idea of a State that takes input events, and that State can hand the events off to substates/other classes.

Share this post


Link to post
Share on other sites
My system is a bit different. I've never heard of anything similar, so I'll drop it in here, and see what you guys make of it.

First of all I have a keyboard manager (at the moment keyboard and mouse are strictly separated, but in the future I want to join them). This keyboard manager gets the key messages from the os (or in your case GLFW, it doesn't really matter). It has a stack of 'keymaps'. These keymaps store all key+action pairs for the current 'screen'. So the game will have it's own keymap and the gui will have another. Different gui systems could have their own keymap if they want. At the moment, only the topmost keymap is active, but this could be changed to allow keyactions to propagate to lower levels.

Whenever a key is pressed/released, the keyboard manager checks the current keymap if there is an action bound to this key. If so, it calls it. How this exactly happens is basically not part of the keyboard manager. In my case, I'm storing functors which I then put into my task handler. You could call them immediately, or you could bind an enum to it. Like I said, this is outside of the actual input processing.

Every frame I call keyboardManager->Process() to process keyaction pairs which want to trigger contineously.

The nice thing about this system is that if the game state changes (a minigame for example), you can simply push a new keymap onto the stack, and the controls will change. Once the minigame is over, you'd pop the keymap again, and the normal controls would be immediately available.

Quote:

The problem is that InputListeners register themselves with the input system when they are created, and deactivate themselves when they are deleted which has lead to some random crashes when an InputListener removes itself which is a pretty tricky situation to handle.
(It seems the trouble stems from one InputListener derived class removes itself when ESC and another callback is called before the Input system can properly remove the now destroyed class)

What you'd want to do in such cases, is first remove it entirely, and only then destroy it. Possibly that might work, but it depends on your architecture of course.

Share this post


Link to post
Share on other sites
Heheh, rick, that was almost exactly what I planned to do, at least in regards to key mappings [grin] At any rate, in my eyes one of the most important part of any input system is the decoupling of actual input from hard-coded commands. As hplus so succinctly put it, listen for named signals, not input events, and instead have a system in between which converts said input events into the named signals.

Share this post


Link to post
Share on other sites
Yes, that is indeed the most important thing. Although my current implementation doesn't do this explicitly, it still isn't hard to do. Just route all the input through a single translation function and only then send it on its way.

Share this post


Link to post
Share on other sites
I do something similar to Mr. Appleton here.

This is the code for my current incarnation of the input handling for my GUI setup. It's to be generally used for 2D games from pong up to turn based strategy.

The GUI system is set up as an n-tree [each node may have an arbitrary number of children]: (pardon the mess, it's still under development)

class basero{
private:
protected:
int visible;
caching_rect_generator *tblr;
public:
virtual int is_visible(){return (visible);}
virtual void visibility(int v){visible=v;}
void toggle_visible(){if (visible){ visibility(0); }else{ visibility(1); }}
virtual void render(){if(visible){rendertree();}}
void deltree();
void rendertree();
virtual void push_top(basero *);


basero *parent;
list<basero *> children;

virtual rect bbox(){return(tblr->fetch());}
virtual void rerect(caching_rect_generator *rt){tblr->del(); tblr=rt;}
virtual depended_rect_generator *dependable(){return(tblr->dependable());}
virtual void tblrreset(){tblr->reset();}

virtual DWORD color(){return(0x00000000);}
virtual void color(DWORD c){}

keybinding kb;
mousetarget mt;

virtual void immediate_close(){delete this;}
virtual void close(){ out_of_order_executables.push_back(storega(voidvoidfunctor(this,&basero::immediate_close)));}
basero *me(){return(this);}
void no_tree_delete();
virtual void no_tree_close();
virtual void extract();

basero(caching_rect_generator *t):tblr(t),parent(0),visible(1){}
virtual ~basero(){
if (parent){
extract();
}
parent=0;
deltree();
tblr->del();
}
};



A few things to note here. First, the various varieties of 'close'. immediate_close is used when the node is to be deleted, well... immediately. As described earlier, this leads to problems if the callback does anything more than simply initiate the callback or if the callback is executed deeper in the tree than the object to be closed. That's where the normal close is used. It simply pushes the immediate_close out to a vector of callbacks which is run between frames.

The other thing to note is the members kb and mt, which are ill-named objects representing keybindings and mousetargets respectively. Yes, that means that every single renderable in this system has its own keymap. Yes, that's slightly inefficient memory-wise. Personally, I find that it's largely insignificant compared to the benefits gained and the memory used by other things.

Now for the code for these two objects, keybinding first:


class keymap{
private:
map<keybindingcode,storega> kmap;
storega *fetch(keybindingcode);
protected:
public:
void keybind(keybindingcode k, guiaction *ga);
bool is_bound(keybindingcode k);
storega bound_func(keybindingcode k);
void operator[](keybindingcode k);
void charin(char c);

keymap(){}
~keymap();
};



class keybinding{
private:
protected:
keymap kmap;
keymap shiftkmap;

bool textentry;
bool opaque;
public:
bool text_mode(){return(textentry);}
void text_mode(bool i){textentry=i;}

bool opaque_mode(){return(opaque);}
void opaque_mode(bool i){opaque=i;}
void toggle_opaque(){if (opaque){opaque=0;}else{opaque=1;}}

void keybind(keybindingcode k, int x, guiaction *ga){if (x!=1){kmap.keybind(k,ga);}else{shiftkmap.keybind(k,ga);}}
bool is_bound(keybindingcode k, int x=0){if (x!=1){return(kmap.is_bound(k));}else{return(shiftkmap.is_bound(k));}}
void textbind(guiaction *ga){keybind(KB_A,0,ga);}

void kbinput(keybindingcode k, int x){if (x!=1){kmap[k];}else{shiftkmap[k];}}
void kbinput(char c){ kmap.charin(c); }

keybinding(bool t=0, bool o=0):textentry(t),opaque(o){}
};



storega [and guiaction] in this context is just a callback functor I use.
keybindingcode is an enumeration of the various keys that the system can accept.

I overloaded operator[] to execute the bound key's callback if it exists, and charin takes a char and passes it to the KB_A callback if the keymap is in text mode. Text mode is a flag set in keybinding to signal the acceptance of textual input to KB_A, rather than binding every single key to pass it's associated character... Opaque mode listed above is a flag to tell the input handler not to pass keyboard input any higher in the tree than here. This would be used in renderables that demark a new window, like a menu. You don't want keyinput going to objects you know are occluded.

Now mousetarget:

class mousetarget{
private:
protected:
public:
storega onclick;
storega onrclick;

void set_onclick(guiaction *ga){onclick=*(new storega(ga)); passup=0;}
void set_onrclick(guiaction *ga){onrclick=*(new storega(ga)); passup=0;}
bool passup;
mousetarget(bool pu=1,guiaction *oc=makega(noop,0,0), guiaction *orc=makega(noop,0,0)):onclick(oc), onrclick(orc), pas
sup(pu){}
~mousetarget(){}
};



A bit simpiler. I'll eventually add mouse-wheel stuff here, but I've not gotten that far yet. The only thing perhaps odd here is the passup member. This is essentially the opposite of the keybinding's opaque_mode. Mousetargets are assumed opaque, and the passup flag tells the input handler to ignore this entry, and look higher in the tree.


Now, why do I do things this way you ask. The primary benefit I find from this is composition of inputs. For example, imagine your standard chat box. A chat box consists of two major parts, a list of text at the top, and a textinput field on the bottom. In this system the textinput field may bind the textinput, left, right, delete, bkspace... and the text list can bind up,down,pgup,pgdown... all independantly of one another. When mixed together, the proper mix of bindings comes through. If it didn't code re-use of these two smaller pieces would be largely irrelevant, since you'd have to manually re-bind everything.

Here are the input handling functions for completeness. guiroot in this context is the root of the tree.


bool techeck(keybinding &nkb, int inkey, int indepth){
//
// Return true iff nkb->textentry==1 and key/depth is a printable char.
//
if (indepth>1){
return(0);
}
if (nkb.text_mode()==0){
return(0);
}
if (inkey>=KB_A && inkey<=KB_NUMPAD9){
return(1);
}
return(0);
}


basero *kbinput(basero *br,int inkey, int indepth){
//
// Run through the keybindings, and activate the guiaction associated with inkey, indepth.
//
basero *rtn=0;
basero *cbr;
br_child_iterator it;

if (br->is_visible()!=1){return(0);}
if (br->kb.is_bound((keybindingcode)inkey,indepth) || br->kb.opaque_mode()){
rtn=br;
}
for (it=br->children.begin();it!=br->children.end();++it){
cbr=kbinput(*it,inkey,indepth);
if (cbr){
rtn=cbr;
}
}
if (br==guiroot && rtn){
// then called from the root object, actually execute the guiaction.
if (rtn->kb.is_bound((keybindingcode)inkey,indepth) && (rtn->kb.text_mode()==0 || !techeck(rtn->kb,inkey,indepth))){
rtn->kb.kbinput((keybindingcode)inkey,indepth);
return(rtn);
}
}
return(rtn);
}

basero *kbinput(basero *br, int inkey, int indepth, char inchar){
//
// Character input. Find the first textentry keybinding and pass the input along.
//
basero *rtn=0;
basero *cbr;
br_child_iterator it;

if (br->is_visible()!=1){return(0);}
if (br->kb.is_bound(KB_A,0) || br->kb.opaque_mode()){
rtn=br;
}
for (it=br->children.begin();it!=br->children.end();++it){
cbr=kbinput(*it,inkey,indepth,inchar);
if (cbr){
rtn=cbr;
}
}
if (br==guiroot && rtn){
if (rtn->kb.is_bound(KB_A,0) && rtn->kb.text_mode()){
rtn->kb.kbinput(inchar);
return(rtn);
}
}
return(rtn);
}

basero *mouse(basero *br, int mousemsg, point mouseat){
//
// Mouse input.
//
basero *rtn=0;
basero *cbr;
br_child_iterator it;

if (!br->is_visible()){return(0);}
if (!br->mt.passup){
if (recthit(br->bbox(),mouseat)){
rtn=br;
}
}
for (it=br->children.begin();it!=br->children.end();++it){
cbr=mouse(*it, mousemsg, mouseat);
if (cbr){
rtn=cbr;
}
}
if (br==guiroot && rtn){
vine *p=new vine(type_manager.fetchid("point"),new point(mouseat));
if(mousemsg==LEFTCLICK){
rtn->mt.onclick.doa(p);
p->nuke(&p);
return(rtn);
}else if (mousemsg==RIGHTCLICK){
rtn->mt.onclick.doa(p);
p->nuke(&p);
return(rtn);
}
}
return(rtn);
}



void kbinput(int inkey, int indepth){ kbinput(guiroot,inkey,indepth); }
void kbinput(int inkey, int indepth, char inchar){ kbinput(guiroot,inkey,indepth,inchar); }
void mouse(int mousemsg, point mouseat){mouse(guiroot,mousemsg,mouseat); }



And here's some example code of the binding process. This is from the textinput constructor (I really need to clean up some of the callback code...)


kb.text_mode(1);
kb.textbind(charin_functor(this));
kb.keybind(KB_DEL,0,voidvoidfunctor(this,&textinput<T,F>::kbdel));
kb.keybind(KB_HOME,0,voidvoidfunctor(this,&textinput<T,F>::gohome));
kb.keybind(KB_END,0,voidvoidfunctor(this,&textinput<T,F>::goend));
kb.keybind(KB_LEFT,0,voidvoidfunctor(this,&textinput<T,F>::goleft));
kb.keybind(KB_RIGHT,0,voidvoidfunctor(this,&textinput<T,F>::goright));
kb.keybind(KB_BKSPACE,0,voidvoidfunctor(this,&textinput<T,F>::bkspc));
kb.keybind(KB_ESC,0,voidvoidfunctor(this,&textinput<T,F>::deactivate));
kb.keybind(KB_ENTER,0,voidvoidfunctor(this,&textinput<T,F>::submit));
mt.set_onclick(voidvoidfunctor(this,&textinput<T,F>::activate));

Share this post


Link to post
Share on other sites
I'm just going to throw my two cents on input design.

I'm designing a fairly flexable engine. WE use messages to communicate between the different layers of the game.
One layer happens to be input.

It does all preprocessing of keys. Bascially when I recieve a key press I translate that based on a key mapping of actions. If the action requires an message i send that message else throw the key stroke away. Messages in my engine are used to update the game state. The exception is the mouse, which the rendering layer calls for the update itself. The only thing the game state gets is mesages from the input layer.
On a move it'll send a move message with a vector. and on a click it'll send a click message and the postion of click. This updates the game state accordingly.

An advantage to this approach is the network layer uses the same message for network communication, so we have a unified message system for use throughout the game.

My layers are like this

File System
Networking
^
|
Input (Preproccessing included) -> Game State -> Rendering and Sound



The File System layer gets special attention as It's not really anything you put into the game engine rather it gets called as a subsystem for loading files or the network layer will access it to stream files and then notify. Things of that nature.

This is still a work in progress but you get the idea. Yeah i went a bit overboard but hey I hope it helps.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!