Interpretation von Key-States bei hoher Framerate

Einstiegsfragen, Mathematik, Physik, künstliche Intelligenz, Engine Design
Antworten
sNIk
Beiträge: 6
Registriert: 12.12.2011, 11:43

Interpretation von Key-States bei hoher Framerate

Beitrag von sNIk »

Hallo zusammen.

Ich frage mich, wie man bei einer hohen Framerate den State eines Buttons, Keys, etc. zuverlässig interpretieren kann. Heutzutage sind die Rechner so schnell, dass man bei einem einzigen Tastendruck gleich mehrere Frames weiter ist und die Unterscheidung zwischen "Taste wurde gedrückt" und "Taste wird gehalten" schwierig ist.

Hier ein Beispiel bei 120 FPS und einem kurzen Tastendruck:

70: keystate: 0
71: keystate: 1
72: keystate: 1
73: keystate: 1
74: keystate: 1
75: keystate: 1
76: keystate: 1
77: keystate: 1
78: keystate: 0

Man sieht hier, dass in Frame 71 eine Taste gedrückt wurde welche diesen State (obwohl kurz angetippt) ganze 7 Frames mitschleppt. Die Events "KeyDown" ( 0->1 ) und "KeyUp" ( 1->0 ) kann man recht leicht identifizieren. Aber wie findet man z.B. heraus, dass eine Taste tatsächlich gehalten wird und nicht lediglich einmal angetippt wurde? Der Vergleich mit dem letzten Frame (1->1) wird ja aus oben genannten Gründen nicht funktionieren.

Was mir einfiele ist die Zeitmessung zwischen "KeyDown" und "KeyUp", um herauszufinden, ob eine Taste tatsächlich gehalten wird. Das würde aber unnötigen Lag (Auslöser ist jetzt "KeyUp" und nicht mehr "KeyDown") bei einfachen Tastendrücken (z.B. ein Schuss) einführen.

Wie wird das Problem in der Wildnis für gewöhnlich gelöst? :)
Zuletzt geändert von sNIk am 02.02.2014, 17:19, insgesamt 1-mal geändert.
Seraph
Site Admin
Beiträge: 1174
Registriert: 18.04.2002, 21:53
Echter Name: Steffen Engel

Re: Interpretation von Key-States bei hoher Framerate

Beitrag von Seraph »

Auf die Schnelle fallen mir die folgenden 'Loesungen' ein:

1. Einen FixedTimeStepLoop mit bedeutend geringerem Update-Cycle nutzen, die Physik-Engines werden sich darueber freuen.

2. Selbst mit dem FixedTimeStepLoop kommt man u.U. nicht drumrum festzulegen, wie lange etwas gedrueckt sein muss um etwas bestimmtes auszuloesen.

3. Darueber nachdenken, ob man je nach Kontext nicht verschiedene Funktionen nutzen kann. Wenn ich meinen Character mit AWSD durch die Gegend bewege, nutze ich vielleicht sowas wie IsKeyDown(). Moechte ich im Menue navigieren, nutze ich evtl. so etwas wie IsKeyPressed(). Die Logik der beiden Funktionen unterscheidet sich halt. Letztere reagiert nur auf das erste KeyDown-Event und wuerde dann erst wieder auf ein erneutes KeyDown nach einem vorherigen KeyUp reagieren. Wenn Du ein Push-System nutzen solltest, musst Du Dir halt was entsprechendes drumrum basteln.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Interpretation von Key-States bei hoher Framerate

Beitrag von Krishty »

sNIk hat geschrieben:Die Events "KeyDown" ( 0->1 ) und "KeyUp" ( 1->0 ) kann man recht leicht identifizieren. Aber wie findet man z.B. heraus, dass eine Taste tatsächlich gehalten wird und nicht lediglich einmal angetippt wurde?
Wofür musst du das denn wissen? Für Navigation und Steuerung reichen ja normalerweise die Up- und Down-Ereignisse. Bei Drag & Drop wartet man ab, ob die Maus um eine bestimmte Distanz bewegt wurde (dann weiß man, dass gehalten bzw. gezogen wird). Bei anderen Lösungen orientiert sich Windows an der Doppelklickzeit.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
sNIk
Beiträge: 6
Registriert: 12.12.2011, 11:43

Re: Interpretation von Key-States bei hoher Framerate

Beitrag von sNIk »

Krishty hat geschrieben:
sNIk hat geschrieben:Die Events "KeyDown" ( 0->1 ) und "KeyUp" ( 1->0 ) kann man recht leicht identifizieren. Aber wie findet man z.B. heraus, dass eine Taste tatsächlich gehalten wird und nicht lediglich einmal angetippt wurde?
Wofür musst du das denn wissen? Für Navigation und Steuerung reichen ja normalerweise die Up- und Down-Ereignisse. Bei Drag & Drop wartet man ab, ob die Maus um eine bestimmte Distanz bewegt wurde (dann weiß man, dass gehalten bzw. gezogen wird). Bei anderen Lösungen orientiert sich Windows an der Doppelklickzeit.
Für gewöhnlich reicht das aus, da du hast Recht. Einige Spiele bieten aber die Möglichkeit mit dem selben Button (im selben Kontext) mal zu triggern, mal aufzuladen. Ist also eher eine reine Verständnisfrage. ;)
joeydee
Establishment
Beiträge: 1039
Registriert: 23.04.2003, 15:29
Kontaktdaten:

Re: Interpretation von Key-States bei hoher Framerate

Beitrag von joeydee »

Im Allgemeinen wird auf das erste Key-Down direkt reagiert, und solange die Taste nicht losgelassen wurde nach bestimmten Zeitabständen (nicht Frames) ein weiteres KeyDown geworfen. Dabei ist für Texteingaben die erste Zeitspanne (Verzögerung) meist etwas größer als der Rest (Wiederholrate).
Heutzutage, wo Barrierefreiheit und Multikulti Stichworte sind, gibt es noch mehr solche Zeitbezogene Einstellungen zur Eingabehilfe, sowie Popups mit Buchstabenalternativen (z.B. Akzente) statt Wiederholungen bei längerem Tastendruck bestimmter Tasten, teils vom Betriebssystem, teils von den Programmen selbst.

Für Dauerfeuer in Spielen gilt aber eigentlich nur: bei Key-Down wird ein Timestamp gespeichert, und in jedem Frame nachgeschaut ob laut Keystatus und Schussfrequenz ein Schuss abgefeuert werden muss, etwa:
if(keyIsDown && currentTime-timeStamp+shootFrequency >= shootFrequency){timestamp+=shootFrequency; shoot()}
Das heißt, der erste Schuss wird sofort berücksichtigt, und wird die Taste kürzer als die Schussfrequenz gehalten, wird auch nur ein Schuss abgegeben.

P.S.: bei Doppelfunktion wie in deinem letzten Post beschrieben hilft nur eine Latenzzeit (um die 100ms dürften reichen), in welcher du entscheidest, ob ein Schuss abgegeben (key up) oder geladen (keyIsDown) wird, das ist richtig.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Interpretation von Key-States bei hoher Framerate

Beitrag von Chromanoid »

Ich hab mir mal vor einiger Zeit eine Statemachine gebaut, die neben Symbolen (für Eingaben) auch gedrückt/nicht-gedrückt und Zeit-Bedingungen (Mindest-Zeit und Maximal-Zeit im aktuellen Zustand) an den Kanten haben kann. Damit kann man jede Tastenkombination inkl. Doppelklick, Dreifachklick etc. ziemlich schön modellieren.
sNIk
Beiträge: 6
Registriert: 12.12.2011, 11:43

Re: Interpretation von Key-States bei hoher Framerate

Beitrag von sNIk »

Chromanoid hat geschrieben:Ich hab mir mal vor einiger Zeit eine Statemachine gebaut, die neben Symbolen (für Eingaben) auch gedrückt/nicht-gedrückt und Zeit-Bedingungen (Mindest-Zeit und Maximal-Zeit im aktuellen Zustand) an den Kanten haben kann. Damit kann man jede Tastenkombination inkl. Doppelklick, Dreifachklick etc. ziemlich schön modellieren.
Haste Sample-Code da? Und wieviel Aufwand war das ca?
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Interpretation von Key-States bei hoher Framerate

Beitrag von Chromanoid »

Kann ich hier heute abend posten. Ich habs allerdings in ActionScript3 mit einem gehörigen Java-Akzent programmiert. Das war kein großer Aufwand. Neu implementieren musst Du es wahrscheinlich sowieso, aber vielleicht kann es ja der Inspiration dienen, ist allerdings recht rudimentär und ich habe das Projekt aufgegeben.

edit: Hab's gestern vergessen, heute abend...
Zuletzt geändert von Chromanoid am 04.02.2014, 09:18, insgesamt 1-mal geändert.
sNIk
Beiträge: 6
Registriert: 12.12.2011, 11:43

Re: Interpretation von Key-States bei hoher Framerate

Beitrag von sNIk »

Danke, ich schaue mir das auf jeden Fall an. :)
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Interpretation von Key-States bei hoher Framerate

Beitrag von Chromanoid »

So jetzt mal meine "Timed Finite State Machine" für Eingaben. Allerdings muss ich dazusagen, dass ich das ganze bisher nur für einen Knopf ausprobiert habe und es bei mehr Knöpfen evt. richtig unübersichtlich wird, aber evt. kann man ja die Zustandssysteme mehrschichtig machen.

Ich habe das ganze für ein Mehrspieler-One-Button-Spieleexperiment gebaut: http://www.colorcombine.com/keyconflict.swf (Projekt ist nach einer Woche auf der "irgendwann"-Liste gelandet :))

Das ist die Steuerung eines Panzers:

Code: Alles auswählen

// Klick -> Losfahren/Stoppen, Doppelklick -> andere Richtung, Dreifachklick -> Umdrehen
public class OneKeyCombo extends TimedFiniteStateMachine
{
	public function OneKeyCombo(maxKeyStrokeDelay:Number,go:Function,stop:Function,turn:Function,flip:Function) 
	{
		super([]);
		
		var state:TimedState;
		
		state = new TimedState("0", []);
		state.addEnterObserver(stop);			
		state.transitions[0] = new TimedTransition("0 (u,2) 1", "u", 1, maxKeyStrokeDelay, Number.MAX_VALUE);
		state.transitions[1] = new TimedTransition("0 (u,1) 2", "u", 2, 0, maxKeyStrokeDelay);			
		states[0] = state;
		
		state = new TimedState("1", []);
		state.addEnterObserver(go);			
		state.transitions[0] = new TimedTransition("1 (u,2) 0", "u", 0, maxKeyStrokeDelay, Number.MAX_VALUE);
		state.transitions[1] = new TimedTransition("1 (u,1) 3", "u", 3, 0, maxKeyStrokeDelay);			
		states[1] = state;
		
		state = new TimedState("2", []);
		state.addEnterObserver(stop);
		state.addEnterObserver(turn);
		state.transitions[0] = new TimedTransition("2 (u,2) 0", "u", 1, maxKeyStrokeDelay, Number.MAX_VALUE);
		state.transitions[1] = new TimedTransition("2 (u,1) 4", "u", 1, 0, maxKeyStrokeDelay);		
		TimedTransition(state.transitions[1]).addTransitionObserver(flip);
		states[2] = state;
		
		state = new TimedState("3", []);
		state.addEnterObserver(stop);			
		state.addEnterObserver(turn);
		state.transitions[0] = new TimedTransition("3 (u,2) 1", "u", 1, maxKeyStrokeDelay, Number.MAX_VALUE);
		state.transitions[1] = new TimedTransition("3 (u,1) 1", "u", 1, 0, maxKeyStrokeDelay);	
		TimedTransition(state.transitions[1]).addTransitionObserver(flip);
		states[3] = state;			
		
		start(0);
	}
}
Hier im Grunde alle beteiligten Klassen:

Code: Alles auswählen

// State
public class TimedState
{
	protected var enterTimestamp: Number;
	public var stateName: String;
	public var transitions: Array /*of TimedTransition*/= [];
	
	private var enterObservers: Array /* of Function */ = [];
	private var leaveObservers: Array /* of Function */ = [];
	
	public function TimedState(stateName:String,transitions: Array /*of TimedTransition*/) 
	{
		this.stateName = stateName;			
		this.transitions = transitions;
	}
	public function addEnterObserver(observer:Function):void {
		enterObservers.push(observer);
	}		
	public function addLeaveObserver(observer:Function):void {
		leaveObservers.push(observer);
	}
	public function removeEnterObserver(observer:Function):void {
		var i:int = enterObservers.indexOf(observer);
		if (i < 0)
			return;
		enterObservers.splice(i, 1);
	}
	public function removeLeaveObserver(observer:Function):void {
		var i:int = leaveObservers.indexOf(observer);
		if (i < 0)
			return;
		leaveObservers.splice(i, 1);
	}
	
	public function enter():void {
		enterTimestamp = getTimer();
		for each(var observer:Function in enterObservers)
			observer();
	}		
	public function leave():void {
		enterTimestamp = -1;
		for each(var observer:Function in leaveObservers)
			observer();
	}
	
	public function get elapsedTimeInState():Number {
		return getTimer() - enterTimestamp;
	}
	
	public function clone():TimedState {
		var clonedTransitions:Array /*of TimedTransition*/= [];
		for each(var transition:TimedTransition in transitions) {
			clonedTransitions.push(transition.clone());
		}
		return new TimedState(stateName,clonedTransitions);
	}
	public function releaseObsevers():void {
		enterObservers = [];
		leaveObservers = [];
		for each(var transition:TimedTransition in transitions)
			transition.releaseObsevers();
	}
}

// Transition
public class TimedTransition
{
	public var targetStateNumber:int;
	public var minElapsedTimeInState:Number=0;
	public var maxElapsedTimeInState:Number=Number.MAX_VALUE;
	public var transitionName:String;
	public var activationSymbol:String;
	public var reenterStates:Boolean = true;
	
	private var transitionObservers:Array /* of Function */= [];
	
	public function TimedTransition(transitionName:String,activationSymbol:String, targetStateNumber:int, minElapsedTimeInState:Number=0, maxElapsedTimeInState:Number=Number.MAX_VALUE) 
	{
		this.transitionName = transitionName;
		this.activationSymbol = activationSymbol;
		this.targetStateNumber = targetStateNumber;
		this.minElapsedTimeInState = minElapsedTimeInState;
		this.maxElapsedTimeInState = maxElapsedTimeInState;
	}		
	
	public function addTransitionObserver(observer:Function):void {
		transitionObservers.push(observer);
	}
	public function removeTransitionObserver(observer:Function):void {
		var i:int = transitionObservers.indexOf(observer);
		if (i < 0)
			return;
		transitionObservers.splice(i, 1);
	}
	public function activate():void {
		for each(var observer:Function in transitionObservers)
			observer();
	}		
	
	public function clone():TimedTransition {
		return new TimedTransition(transitionName+"", activationSymbol+"", targetStateNumber, minElapsedTimeInState, maxElapsedTimeInState);
	}
	
	public function releaseObsevers():void {
		transitionObservers = [];
	}
}

// FSM
public class TimedFiniteStateMachine
{
	public static const WILDCARD_SYMBOL:String = "*";
	public var states:Array /*of TimedState*/= [];
	protected var _curState:TimedState;
	protected var _curStateNum:int = 0;
			
	public function TimedFiniteStateMachine(states:Array) 
	{
		this.states = states;
	}
	public function get curState():TimedState {
		return _curState;
	}
	/**
	 * does not execute leave with the previous state.
	 */
	public function set curState(value:TimedState):void {
		if (_curState == value)
			return;
		var stateIndex:int = states.indexOf(value);
		if (stateIndex < 0){
			throw new Error("State is not part of the state machine."); 
		} 
		else 
		{		
			_curStateNum = stateIndex;
			_curState = states[_curStateNum];						
			_curState.enter();
		}			
	}
	public function get curStateNum():int {
		return _curStateNum;
	}
	/**
	 * does not execute leave with the previous state.
	 */
	public function set curStateNum(value:int):void {
		if (value < 0 || value >= states.length) {
			throw new Error("No state with state number "+value+" in state machine."); 
		}
		else
		{
			_curStateNum = value;
			_curState = states[_curStateNum];						
			_curState.enter();
		}			
	}
	public function start(startStateNumber:int): TimedFiniteStateMachine{
		_curStateNum = startStateNumber;
		_curState = states[_curStateNum];			
		_curState.enter();			
		return this;
	}		
	public function input(symbol:String) {
		if (curState != null)			
		{
			var elapsedTime:Number = _curState.elapsedTimeInState;
			for each(var transition:TimedTransition in _curState.transitions) {
				if (transition.maxElapsedTimeInState > elapsedTime && transition.minElapsedTimeInState <= elapsedTime && (transition.activationSymbol == symbol|| transition.activationSymbol==TimedFiniteStateMachine.WILDCARD_SYMBOL) ) {
						if (_curStateNum == transition.targetStateNumber && !transition.reenterStates) {
							transition.activate();
						}
						else
						{
							_curState.leave();								
							transition.activate();		
							_curStateNum = transition.targetStateNumber;
							_curState = states[_curStateNum];						
							_curState.enter();
						}
						return;
				}					
			}
		}
	}		
	public function clone():TimedFiniteStateMachine {
		var clonedStates:Array /*of TimedState*/= [];
		for each(var state:TimedState in states) {
			clonedStates.push(state.clone());
		}
		return new TimedFiniteStateMachine(clonedStates);
	}
	public function releaseObsevers() {
		for each(var state:TimedState in states) {
			state.releaseObsevers();
		}
	}
}
sNIk
Beiträge: 6
Registriert: 12.12.2011, 11:43

Re: Interpretation von Key-States bei hoher Framerate

Beitrag von sNIk »

Danke, ich werde den Code heute Abend studieren. ;)
Antworten