Citrus-Engine icon indicating copy to clipboard operation
Citrus-Engine copied to clipboard

[proposal] Input action as events

Open gsynuh opened this issue 12 years ago • 18 comments

Hello there, I'd like to know if you think input could be working only with signals now - for more precision.

input would have an output signal on which we can connect any listener function : An input controller would send a signal (exactly as it is doing now) to input when triggering on or off an action (or changing a continuous action). As before, these will be kept in a list. justDid, isDoing and hasDone would check on that list (again this doesn't change) But there would be a output signal like so

_ce.input.output.add(_handleActions);

private function _handleActions(a:InputAction):void { if(a.name == "jump") { //do jump. } }

the Input.update() would dispatch output on every frame for actions that are continous, so on every "input frame" you would get a call to _handleActions as well to update action values that are changed. The good thing is, some people like working with events, and in any case this is more precise than quantizing actions into frames. The bad thing (but I'm not sure of it) is too many calls. But to remedy that, maybe we can actually create a custom signal just to have special arguments to it when adding listeners such as :

_ce.input.output.add(_handleBeginActions,InputAction.BEGAN); this second argument, would mean _handleBeginActions will only be triggered when action is of type InputAction.BEGAN . (if signals can't handle an extra argument, I'm sure it's easily doable)

This also means we can have a fairly decent and precise "mouse input controller" just to have the "touch screen" event and everything.

The problem is still that if we don't keep the current system, then justDid() will not reply if we Key_UP/Key_DOWN in the same frame - the current system makes sure that action stays long enough in the list for justDid to find it. so we can't go "signals only" unless there's a good reason to so the two systems must co-exist.

If that does sound good I will look into is asap, unless you can find any holes in my reasoning and I'd be more than happy to discuss it more to see if we figure out the pros and cons

gsynuh avatar Feb 17 '13 12:02 gsynuh

Hey, I think giving the ability to use Signal is very cool. But it can't be the only way.

Using Signal:

  • no more if to check an action in a update function.
  • a simple way to separate action using different functions -> but may rise problem if dependecy between actions.

I would create 3 Signals: isDoing, justDid, hasDone.

_ce.input.output.add(_handleBeginActions,InputAction.BEGAN); This seem to be pretty powerful, but don't know if it can be achieved. I'm not keen of changing external libraries.

One of the top feature will be to be able to register/listen Signal on a specific action directly. However it means that we need to create an action class. myAction.add(onJumping); function onJumping(phase:String){};

Also we have to take care about channel.

alamboley avatar Feb 17 '13 14:02 alamboley

  • "no more if to check an action in a update function." - Yes we can keep both systems using the action queues (I strictly do not want this to go away because its cool and simple.
  • What do you mean by dependency between actions? - we could also "add a listener to a channel instead of by action type"
  • "I'm not keen of changing external libraries." - what if I simply extend it ?

And if extending Signal doesn't sound good, a custom callback system wouldn't hurt.

Since you agree that it's a good idea but you're worried about technicalities (and I'm worried too) I guess I will try to do an example project using these ideas. I need your feedback on extending external libraries , otherwise I'll go for a custom callback system, unless this is also a concern

I know we have more pressing matters than this so it could wait (I don't see anyone complaining about the input system yet)

gsynuh avatar Feb 18 '13 14:02 gsynuh

Well, I think an Action class is the key. It will dispatch its action with the "mode" concerning: hasDone, isDoing... And then we will listen it: input.action("jump").hasDone.add.

  • By dependency between actions: I mean, we use always a function checking the different input and making then actions using if/ else if. If we use different functions, it may be harder to track the action.

What do you think?

alamboley avatar Feb 18 '13 14:02 alamboley

obviously this can't be done for 3.2, It should be postponed like the ui think I suggested.

so , I didn't reply and I'm sorry. I like the input.action("jump").hasDone.add(_onJumpOver) idea, but does action("jump") return an object that has 3 signals (justDid,isDoing, hasDone)? if so that means each possible action would have 3 signals? that makes a lot of signals.

I was thinking of a similar system to Touch Events, we handle touch by comparing the touch phase. so maybe, instead of that, input.action("jump").add(_onJump) would be better... then we just have to check on the phase of the jump action - and we have at max, 3 if/else, like we would with Touch.

it may even be possible to make it look like input.listen("jump",_onJump) and the "listener" would receive the inputAction object which already exists and has the phases defined : _onJump(action:InputAction)

if(action.phase = InputAction.BEGIN) { ///do something } else if (action.phase = InputAction.ENDED) { ...bla }

I find your "input.action("jump").hasDone.add(_onJumpOver)" to be perfect, readable and easy. however behind that there is going to be a lot of checks anyway to figure out when to dispatch what - and also this may mean every action object needs 3 signals (which would need to be temporary) so I think its bad for the memory.

Maybe I didn't fully understand where you were going so please correct me. I just tried to find a compromise here .

this can be done along with the new InputController we've talked about that I will need so it's not going to waste much time


I also find this method to be more precise than handling input in an update. I have had some complaints of keyboard not being so responsive through the update loop (strangely, and it may only be a placebo effect) and maybe the new way of handling the updates with frim that you are trying to work out will hurt the responsiveness even more?

gsynuh avatar Mar 29 '13 00:03 gsynuh

I will have to make lots of test with the frim indeed.

Indeed cloning the TouchPhase system sounds good. People are aware of it, so it won't be a problem!

So its: input.action("jump").add(_onJump) VS input.listen("jump",_onJump)

The second is better I think, and it rises me one point: why not using addEventListener ?

alamboley avatar Mar 29 '13 08:03 alamboley

you're totally right, I made the function look like a addEventListener voluntarily, I just stopped thinking about it - because I had a misunderstanding of the event dispatcher system. Input is extending nothing, It can be an EventDispatcher I guess!

gsynuh avatar Mar 29 '13 09:03 gsynuh

Yep absoluetly, and it will make sense!

I think it's better to use Event than Signals here because people are more familiar with them in those case. Also people shouldn't forget to remove the listener, but most of them won't have problem with that.

I think this event will be very useful for basic actions : a game with only one touch area (Tiny Wings, Ski Safari, etc).

alamboley avatar Mar 29 '13 09:03 alamboley

Reporting back with my tests I thought I would share.

Just tried a new InputController (an Accelerometer) here's roughly how it looks like

package citrus.input.controllers 
{
    import citrus.input.InputController;
    import flash.events.AccelerometerEvent;
    import flash.sensors.Accelerometer;
    /**
     * ...
     * @author gsynuh
     */
    public class AccelerometerInput extends InputController
    {
        private var _accel:Accelerometer;
        
        public function AccelerometerInput(name:String,params:Object) 
        {
            super(name, params);
            if (!Accelerometer.isSupported)
                trace(this, "Accelerometer is not supported");
            else
            {
                _accel = new Accelerometer();
                _accel.addEventListener(AccelerometerEvent.UPDATE, onAccelerometerUpdate);
            }
            
        }
        
        public function onAccelerometerUpdate(e:AccelerometerEvent):void
        {
            triggerVALUECHANGE("accelX", e.accelerationX);
            triggerVALUECHANGE("accelY", e.accelerationY);
            triggerVALUECHANGE("accelZ", e.accelerationZ);
        }
        
    }
}

reminder : when triggerVALUECHANGE is called, it will make it so that for the first time justDid() would return true for example, and then continuously return true for isDoing() .

I transformed Input into an eventDispatcher, I'm only dispatching a new even when triggerVALUECHANGE is sending the action to Input ... for the sake of testing - it needs to be more complicated than that but this was just a test.

Now my first question was, what should be my event to dispatch??? fortunately it was logical, I already had a data structure to describe actions : my event is an action ! and so I set InputAction to be an extension of Event. it seems almost too easy.

(in fact so far, transforming Input into an event dispatching system costs 2 or 3 lines of code.)

here's a test state now so you can see how the code could be used in any scenario:

//initialize
_accelInput = new AccelerometerInput("accel", null); //the input controller.
_ce.input.addEventListener("accelY", _onAccelY);
//and here's a basic idea of how to use that
private function _onAccelY(a:InputAction):void
        {
            if (a.phase == InputAction.BEGAN) // InputActions do have phases stored, so I can compare them like I would with Touch.
            {
                trace(a, 'BEGAN');
            }
            q.color = Color.argb(1,255* a.value ,0,0); // because of the original design of InputAction which was made to hold a Number as data, I can use a.value to get the accelerometer value straight away.
        }

So this code looks very familiar to how you would handle Touch for example, so I think people might like it - at least I hope.

It might start the debate on whether to keep justDid etc or not. in the real world, input is always event based (even if the event is continous) , right now we are frame based, and that's weird. still, I'll try and see if both systems can coexist nicely.

gsynuh avatar Mar 30 '13 01:03 gsynuh

I really like this input system with events. However I'm wondering how we manage several events.

Let's take the hero, we will have to listen for jump, right, left, and duck actions. Obviously, we want them to call the same function and make if/switch there. In Starling when we enable multi-touch, we take a vectors of Touch in our event listener. How will we do that here? It would be cool to port the hero update function to this new system, and see how it goes ;)

alamboley avatar Mar 31 '13 08:03 alamboley

I'm not sure what you want vectors of in the case of input (if not for touch on display objects), but you made me realize that I missed something obivous here : the channels.

with what I have tested right now, anything receives the "accelX" ... so channels are of course no longer considered in that case.

in starling and flash, display objects are visual targets and events go through each of them because each display object is an event dispatcher on which we can listen events.

I have a solution for that, that may be considered, every (or almost every) citrus object could have an input property, which would itself be an eventDispatcher with the added fact that it would actually filter out unwanted actions - the ones with the wrong channel. effectively this would be how the code would look in the end :

hero.input.addEventListener("left",_onLeft);

and in this case we would be sure to receive the "left" action intended for hero :

hero.input would be an event dispatcher that forwards events according to the channel number (or maybe multiple channels ?)

we would need to do hero.input.setChannel(1) so the input object would only forward actions meant for channel 1 - by default it would be channel 0.

this way hero.input.addEventListener("left",_onLeft); will work fine.

maybe now to prevent unecessary calls, channels might actually need to become real objects like you wanted , so that inside Input the following happens :

  • create default 16 channel objects which are event dispatchers.
  • on action, dispatch action to corresponding channel.
  • when using hero.input.setChannel(0); , then the hero.input eventDispatcher registers itself as a listener on the right channel

this way input doesn't actually filter anything, it only forwards, and we'll be sure to receive the right thing all the time.

I'll try that and see how it goes actually...

remember that Input would have a form of "onGoing" phase - on each Input update it will trigger events that are onGoing to know if we are still pressing the "jump" key for example. this is not a problem but this is the only way I can see isDoing() replaced.

gsynuh avatar Mar 31 '13 12:03 gsynuh

Firstly we should create a custom event class, so we will be able to give parameters to our event!

hero.input.addEventListener("left",_onLeft); is bad, it should be : hero.input.addEventListener(InputEvent.LEFT,_onLeft);

So when we dispatch the event, we are able to give parameters: dispatchEvent(new InputEvent(InputEvent.LEFT, currentChannel, currentPhase));

This way on our _onLeft function, if the currentChannel doesn't correspond to our object's channel, we make a simple return and that's all.

Concerning the vector, take a look on how multitouch is managed in Starling, and let me know what you think.

alamboley avatar Apr 01 '13 10:04 alamboley

actually, InputEvent.LEFT is only a string refering to the "left" action. people creating their own actions will have to modify the InputEvent class you are talking about to add the constant, that's hard if you're using the CE swc, what I'm proposing is "left" because it can also be "timeshift" or "special" or anything the person created...

I'm one step ahead of you though, InputAction on my test project IS an EVENT !!! I'm dispatching InputAction !!! so I have the action name, the action value etc :

private function _onLeft(a:InputAction):void { //we have access to a.phase, a.name, a.value, a.controller etc... even a.channel ! }

so don't worry about this , InputAction was a simple "data structure", i just added "extends Event" and I'm now able to dispatch an action from Input !

my tests with accelerometer show that it works perfectly.

if Hero has an inputComponent which listens to the global Input, such compononent will be able to re-dispatch only actions for the defined channel inside Hero by comparing the received InputAction.channel value to its own inputChannel value !

  • To tell the truth, I was suprised it was going to be that easy.

for vectors, I don't think you are likely to trigger two actions at the exact same time, except for "on going" actions which could be sent by batches... so you would do

inputComponent.addListener(Input.ACTION,_onAction);

and there you would receive a special event from which you can get a vector of all actions going on in this frame - this is not a problem to add - like a touch event is actually all touches possible or only one, well here you can either listen for all possible actions on that channel or listen to specific actions such as "jump" etc...

I understand InputAction.LEFT is prettier to reference, but apart from the basic left right up jump actions, people would create their own actions and would reference them with their own strings anyway so this is not really a problem.... if they want, they can store these strings into a constant to get the same look GameConstants.ACTION_SPECIAL:String = "special";

and add an action to a keyboard key with addAction(GameConstants.ACTION_SPECIAL,Keyboard.NUMBER_0);

this way they can do the following : inputComponent.addListener(GameConstants.ACTION_SPECIAL,_onSpecial); and it will work.


If it's so easy, then why don't I add it right now you might ask. Well my problem right now is making this system compatible with the current system with justDid() isDoing() which are not event based but "frame based" - and I'm not yet sure if I can make them co-exist... and if I can, I'm not sure how this will affect performance.

The bottom line here is, all the worries you expressed in your reply are completely normal - because I had the same, but now with a solution for each problems you've expressed, I hope you now are less doubtful about the idea.

of course the best thing to have right now, is an example... I will make an example project, using a modified Input, InputAction as Events, and an inputComponent so it will be testable and clear. Although for this example I think I'll remove justDid() isDoing() etc... which are going to become a burden until I manage to make them co-exist.

gsynuh avatar Apr 01 '13 15:04 gsynuh

Ok perfect for InputAction as Event. However we have to create a class for InputEvent.LEFT. Using string is too bad... it sucks really. People will have to check how it is written somewhere in the engine. Here they will just need to extend InputEvent to add their own 'command". If only AS3 had Enum...

Mixing Hero and inputComponent doesn't sound good for me. Because adding entity component to a no entity object is bad. And the (basic?) Hero should not be an entity.

Don't forget that for the Hero, you have often left/right pressed and jump comming sometimes.

alamboley avatar Apr 02 '13 08:04 alamboley

Well until now if people created their own action, they were refering to them with strings, this didn't seem to be a problem :/ the extra work of creating a class to store constant strings can still be done if you want.

Ok let's forget about inputComponent, I think the name is wrong then, let's call it only input. The event system in starling and flash works because every display object is an event dispatcher so we can just addEventListener to any display objects. However in Citrus Engine, nothing is an event dispatcher by default (and that's ok because it would be too heavy and stupid) so we absolutely need to have an object that hero holds to receive the right kind of input. otherwise we would be doing _ce.input.channel[0].addEventListener() or something like that but this means we need to create a set of channel objects to dispatch filtered actions through them, and maybe sometimes no one will be listening and we'll lose some resources this way. the idea behind having an input object to listen to wherever we want is that it would plug into Input only when used and only with the channel it needs etc...

I understand left can be pressed with jump, But if they are events, the Key_Down events will never come together, events do not collide, they are queued and are calling the listener one after the other so you can't get both actions at once.

What I think you mean to say is that multiple keys can be down in the same frame, or multiple keys can have started in the same frame. that's very different :

that's why the following suggestion : addListener(Input.ACTION,_onAction); could actually be triggered on each frame / event... and so the event that we would get can contain a collection of actions that started but didn't end yet, much like Touch I think.

  • with the current system, you can have two justDid("left") justDid("jump") in the same condition, for the only reason that actions are "quantized" to a frame. what we are really asking is "have you just started in this frame?" . but both keys have never been received at the same time. the time between both presses was just smaller than your frame rate. unfortunately with events this goes away

If you really want to check for a left + jump combo, then you wait for left or jump to be pressed. if so you don't act. if then you receive jump or left and the other key of the combo is not up, then the combo is complete.

checking the combo with the current system you need one of those conditions : justDid("jump") && isDoing("left") justDid("left") && isDoing("jump")

because justDid("left") justDid("jump") will only happen if your player can press both in much less than 16ms

for that, the vector thing you are talking about, would each time return a vector of old actions that didn't end + the newest one , or old actions that didn't end but one or all might have changed values.

Again, I'll show up with something soon cause its not that our point of view diverge, I agree with you, it's just I happen to not be using the right vocabulary (when I talked about a component) I think a concrete code example , as always, might be clearer than words in this case.

gsynuh avatar Apr 02 '13 12:04 gsynuh

The problem is not people creating their own action: they can name it whatever they want. The problem is to know the action name defines by the engine. Using a string which is never referenced as a constant is hard to find.

Ok I see the issue for the events, Vectors... however I don't see how it can be easily resolve using events: we have to think the hero may use 3 keys at the same and even more : space to jump, left, click to shoot and mouse to aim. It is easily managed via the update function, but how it is going with listener?

alamboley avatar Apr 02 '13 13:04 alamboley

A bit of news :

so for the default action names, I have moved up/down/left/right/jump/duck to InputAction : you would now do the following :

if (_ce.input.isDoing(InputAction.JUMP))

the phases are in a new class InputPhase (like TouchPhase) and so the following can be done when receiving an action :

if (a.name == InputAction.JUMP && a.phase == InputPhase.ENDED)

this is the equivalent to

if (_ce.input.hasDone(InputAction.JUMP))

as for an event system, here is some code :

//listen to all/any actions and use getActions() to filter them by channel or get the full list.
_ce.input.addEventListener(InputEvent.ACTIONS, _onAction);
            
//get only "jump" actions regardless of channel
_ce.input.addEventListener(InputAction.JUMP, _onJump);
private function _onAction(e:InputEvent):void
        {
            var actions:Vector. = e.getActions();
            var a:InputAction;
            for each(a in actions)
            {
                if (a.name == InputAction.JUMP && a.phase == InputPhase.BEGIN)
                {
                    trace("JUMP BEGIN");
                }
                
                if (a.name == InputAction.JUMP && a.phase == InputPhase.ENDED)
                {
                    trace("JUMP ENDED");
                }
            }
        }

you will notice

var actions:Vector. = e.getActions();

which gets the full list of actions up until the latest triggered actions. if I press a new key, this InputEvent is triggered with all keys that are still being pressed along with the newest one so I can react to combos for example.

var actions:Vector. = e.getActions(2);

will do the same, only it will give me what happens on channel 2.

as for the _onJump listener here it is

private function _onJump(a:InputAction):void
        {
            if (a.phase == InputPhase.BEGIN)
            {
                trace("jump listener : jump BEGIN");
            }
            
            if (a.phase == InputPhase.END)
            {
                trace("jump listener : jump END");
            }
        }
        

-- note that my test has been done on a project that has a default Hero , and he still works with its default code (justDid etc...) .

for the hero to listen by default to a certain channel, a "middle man" needs to exist, such as an "inputFilter" object that would pre-filter and redispatch "inside the hero" for example :

//In Hero or any class that needs input
private var input:InputFilter = new InputFilter(defaultChannel);
// later...
input.setChannel(0) // get only actions from channel 0
//or even
public function set inputChannel(value:uint):void
{
   input.setChannel(value);
}
// and then , usage :
input.addEventListener(InputAction.JUMP,_onjump);

input will pre-filter and dispatch signals only with the defined channel in a way that hero listening to this input will not be getting anything from anywhere else. he still can listen to the global _ce.input though. - but this is something I'm not completely sure about yet. InputEvent that I displayed earlier does already filter by channel with getActions() so I don't know if this is necessary or not yet.


The system works, the only problem is that its not very optimized yet

  • pressing all keys at once or at high speeds, seems to cost a bit more than I would expect. Also Keyboard takes 6% of frame time - when pressed randomly and fast. and Starling.onKey is called (I had no idea this existed) which takes 5% - in a way Starling.onKey adds on top of the overall time so that it doubles it, even though I'm not actually using Starling events / keyboard - I wonder why.

gsynuh avatar Apr 06 '13 12:04 gsynuh

very interesting thing for combos, if the order is important for example JUMP + LEFT here's how you'd do it:

private function _onAction(e:InputEvent):void
{
var actions:Vector. = e.getActions();
var a:InputAction;
var doingjump:boolean = false;
for each(a in actions)
   if (a.name == InputAction.JUMP && a.phase < InputPhase.END)
     doingjump = true;
for each(a in actions)
   if(a.name == InputAction.LEFT && a.phase == InputPhase.BEGIN && doingjump)
      doJumpLeftCombo();
}

unfortunately here we have to cycle twice in the list of actions, as we are not sure that LEFT will be after JUMP in the list. if we were sure we'd only have to cycle once.

if InputEvent has a reference to the last action in a way, this could be done like so

private function _onAction(e:InputEvent):void
{
var actions:Vector. = e.getActions();
var a:InputAction;
var doingjump:boolean = false;
for each(a in actions)
   if (a.name == InputAction.JUMP && a.phase < InputPhase.END)
     doingjump = true;
if(e.lastAction.name == InputAction.LEFT && e.lastAction.phase == InputPhase.BEGIN && doingjump)
      doJumpLeftCombo();
}

which is much easier for order important combos. if the order isn't important then we cycle once in any case.


Something else I need to say. justDid() doesn't base itself on BEGIN but on BEGAN. because BEGIN can happen anytime in a frame, even after an update cycle, then we might not catch BEGIN in Hero and it would fail.

such action is sure to be advanced to its next phase, BEGAN, on the next frame,because Input advances every phase in its independent update system, and in that case justDid in Hero.update is sure to catch that action.

because of that, the current system is always one frame behind, but that's the cost of checking that action correctly. sometimes it would feel that the game is not very responsive because of that... specially if your frame rate drops a lot.

this is one of the reasons why I'm advocating for input to be transformed into an event only system - but that's a debate that needs to be opened as well.

gsynuh avatar Apr 06 '13 12:04 gsynuh

threw together a little example code, much clearer than what we have here :

https://github.com/gsynuh/CEpool/blob/master/src/states/InputEventDemoState.as

it shows the old and new system co-existing.

gsynuh avatar Apr 07 '13 12:04 gsynuh