Richtige Methode zum Abstrahieren eines XBox-Controllers

12

Ich habe einen XBox360-Controller, den ich als Eingabe für eine Anwendung verwenden möchte.

Was ich nicht herausfinden kann, ist die Best-Practice-Methode, um dies über eine Schnittstelle herauszustellen.

Hinter den Kulissen ist die Klasse, die die Controller verwaltet, auf den Status der Abfragetasten angewiesen.

Ich habe anfangs etwas Link ausprobiert:

Event ButtonPressed() as ButtonEnum

wo ButtonEnumwar ButtonRed, ButtonStartusw ...

Dies ist insofern etwas eingeschränkt, als es nur das Drücken von Tasten und nicht das Halten / Mustern (zweimal drücken usw.) unterstützt.

Die nächste Idee war, einfach den Schaltflächenstatus für die App freizugeben, z

Property RedPressed as Boolean
Property StartPressed as Boolean
Property Thumb1XAxis as Double

Dies ist sehr flexibel, erzwingt jedoch zu viel Arbeit in der App und erfordert das Abfragen der App - ich würde es vorziehen, wenn es nach Möglichkeit ereignisgesteuert ist.

Ich habe darüber nachgedacht, mehrere Ereignisse hinzuzufügen, zB:

Event ButtonPressed(Button as ButtonEnum)
Event ButtonPressedTwice(Button as ButtonEnum)
Event ButtonHeldStart(Button as ButtonEnum)
Event ButtonHeldEnd(Button as ButtonEnum)

aber das scheint ein wenig klobig zu sein und war ein echtes Problem auf dem Bildschirm "Schaltfläche binden".

Kann mir jemand bitte den "richtigen" Weg weisen, um mit Eingaben von Controllern umzugehen.

NB: Ich benutze SlimDX in der Klasse, die das Interface implementiert. Dadurch kann ich den Status sehr leicht ablesen. Alle Alternativen, die mein Problem lösen würden, werden ebenfalls geschätzt

Basic
quelle

Antworten:

21

Es gibt kein perfektes Mapping, das Ihnen eine plattformspezifische Abstraktion gibt, da offensichtlich die meisten für einen 360-Controller sinnvollen Bezeichner für einen PlayStation-Controller falsch sind (A anstelle von X, B anstelle von Circle). Und natürlich ist ein Wii-Controller eine ganz andere Sache.

Der effektivste Weg, um damit umzugehen, ist die Verwendung von drei Implementierungsebenen. Die untere Ebene ist vollständig plattform- / steuerungsspezifisch und weiß, wie viele digitale Tasten und analoge Achsen verfügbar sind. Es ist diese Ebene, die weiß, wie der Hardware-Status abgefragt wird, und es ist diese Ebene, die sich gut genug an den vorherigen Status erinnert, um zu wissen, wann eine Taste gerade gedrückt wurde oder ob sie länger als einen Tick gedrückt wurde oder ob sie nicht gedrückt wurde . Davon abgesehen ist es dumm - eine reine Zustandsklasse, die einen einzelnen Controllertyp darstellt. Der wahre Wert besteht darin, den Kern des Abfragens des Controller-Zustands von der mittleren Ebene zu abstrahieren.

Die mittlere Ebene ist das eigentliche Control-Mapping von realen Buttons zu Spielkonzepten (zB A -> Jump). Wir nennen sie Impulse statt Buttons, weil sie nicht mehr an einen bestimmten Controllertyp gebunden sind. Auf dieser Ebene können Sie Steuerelemente neu zuordnen (entweder während der Entwicklung oder zur Laufzeit auf Wunsch des Benutzers). Jede Plattform verfügt über eine eigene Zuordnung von Steuerelementen zu virtuellen Impulsen . Sie können und sollten nicht versuchen, sich davon zu lösen. Jeder Controller ist einzigartig und benötigt ein eigenes Mapping. Tasten können mehr als einem Impuls zugeordnet sein (abhängig vom Spielmodus), und mehr als eine Taste kann demselben Impuls zugeordnet sein (z. B. A und X beschleunigen, B und Y verzögern beide). Das Mapping definiert all das,

Die obere Ebene ist die Spielebene. Es braucht Impulse und es ist egal, wie sie erzeugt wurden. Vielleicht kamen sie von einem Controller oder einer Aufnahme eines Controllers, oder vielleicht kamen sie von einer KI. Auf dieser Ebene ist es dir egal. Was Sie interessiert, ist, dass es einen neuen Sprungimpuls gibt oder dass der Beschleunigungsimpuls fortgesetzt wurde oder dass der Tauchimpuls diesen Tick einen Wert von 0,35 hat.

Bei dieser Art von System schreiben Sie die unterste Ebene einmal für jeden Controller. Die obere Schicht ist plattformunabhängig. Der Code für die mittlere Ebene muss nur einmal geschrieben werden, die Daten (das Remapping) müssen jedoch für jede Plattform / jeden Controller erneut erstellt werden.

MrCranky
quelle
Dies scheint ein sehr sauberer und eleganter Ansatz zu sein. Vielen Dank!
Basic
1
Sehr schön. Viel besser als meins: P
Jordaan Mylonas
3
Sehr schöne Abstraktion. Aber seien Sie vorsichtig bei der Implementierung: Erstellen und zerstören Sie nicht für jede Benutzeraktion ein neues Impulsobjekt. Pooling verwenden. Müllsammler wird es Ihnen danken.
Grega G
Absolut. Ein statisches Array mit der maximalen Anzahl gleichzeitiger Impulse ist fast immer die beste Option. da fast immer nur eine Instanz jedes Impulses gleichzeitig aktiv sein soll. In den meisten Fällen enthält dieses Array nur wenige Elemente, sodass es schnell durchlaufen werden kann.
MrCranky
@grega danke euch beiden - daran hatte ich nicht gedacht.
Basic
1

Ehrlich gesagt würde ich sagen, dass die optimale Benutzeroberfläche stark von der Verwendung im Spiel abhängt. Für ein allgemeines Anwendungsszenario würde ich jedoch eine zweistufige Architektur vorschlagen, die ereignisbasierte und abrufbasierte Ansätze trennt.

Die untere Ebene kann als "abrufbasierte" Ebene betrachtet werden und zeigt den aktuellen Status einer Schaltfläche an. Eine solche Schnittstelle könnte einfach 2 Funktionen haben, GetAnalogState(InputIdentifier)und GetDigitalState(InputIdentifier)wo InputIdentifierist ein Aufzählungswert, der angibt, gegen welche Schaltfläche, welchen Auslöser oder welchen Stick Sie prüfen. (Wenn Sie GetAnalogState für eine Schaltfläche ausführen, wird 1.0 oder 0.0 zurückgegeben, und wenn Sie GetDigitalState für einen analogen Stick ausführen, wird true zurückgegeben, wenn ein voreingestellter Schwellenwert überschritten wird, oder andernfalls false.)

Die zweite Schicht würde dann die untere Schicht verwenden, um Ereignisse bei Statusänderungen zu generieren und es Elementen zu ermöglichen, sich für Rückrufe zu registrieren (C # -Ereignisse sind großartig). Diese Rückrufe würden Ereignisse für Presse, Veröffentlichung, Tippen, Langhalten usw. beinhalten. Für einen analogen Stick könnten Sie Gesten einschließen, z. B. QCF für ein Kampfspiel. Die Anzahl der angezeigten Ereignisse hängt davon ab, wie detailliert ein Ereignis ist, das Sie auslösen möchten. Sie könnten einfach ein einfaches abfeuern, ButtonStateChanged(InputIdentifier)wenn Sie alles andere in einer separaten Logik behandeln möchten.

Wenn Sie also den aktuellen Status einer Eingabetaste oder eines Steuerknüppels überprüfen müssen, überprüfen Sie die untere Ebene. Wenn Sie eine Funktion bei einem Eingabeereignis einfach auslösen möchten, registrieren Sie sich für einen Rückruf von der zweiten Ebene.

Jordaan Mylonas
quelle