Jak poprawnie sprawdzić, czy kontroler jest podłączony do peceta?

0

Usiłuję napisać poprawnie działający kod odczytu stanu przycisków kontrolera (gamepada jak kto woli), podłączonego na USB. Wszystko działa elegancko, jeśli kontroler jest podłączony — tutaj nie mam absolutnie żadnych problemów. Jednak jeśli włączę program, a kontroler nie jest podłączony, sprawdzanie jego stanu (podłączony i jeśli tak, to pobranie stanu przycisków) trwa od 100ms do nawet 300ms, przez co framerate spada z 60fps do 3-4fps, w porywach do 10fps.

Jeśli w takim przypadku podłączę kontroler, framerate wraca do normy. Jeśli znów odłączę kontroler, to nadal wszystko działa prawidłowo — framerate jest w normie i już aż do końca sesji nic się z nim nie stanie. Problem występuje tylko przy rozruchu, jeśli gamepad nie jest podłączony. I problem jest taki, że funkcja pobierająca stan klawiszy i zwracająca kod błędu, po wystartowaniu programu widzi kontroler jako podłączony, pomimo tego, że nie jest. Dopiero podłączenie i odłączenie gamepada sprawia, że jego widoczność jest prawidłowa (jak jest to jest, a jak nie ma to go nie ma).

Stan podłączenia kontrolera oraz pobranie stanu jego przycisków realizuję za pomocą funkcji joyGetPosEx. Kod wygląda tak:

FStatus := Default(JOYINFOEX);
FStatus.dwSize := SizeOf(FStatus);
FStatus.dwFlags := JOY_RETURNX or JOY_RETURNY or JOY_RETURNBUTTONS;

FConnected := joyGetPosEx(JOYSTICKID1, @FStatus) = JOYERR_NOERROR;

if FConnected then
begin
  UpdateArrows();
  UpdateButtons();
end
else
  Reset();

Niestety joyGetPosEx domyślnie widzi kontroler jako niepodłączony, zwraca kod JOYERR_PARMS i ssie, a dopiero po pierwszym podłączeniu zaczyna go prawidłowo widzieć/nie widzieć i już nie ssie — jeśli kontroler jest podłączony to zwraca kod JOYERR_NOERROR (prawidłowo), a jeśli go znów odłączę, to kod JOYERR_UNPLUGGED (też prawidłowo).

Moje metody UpdateArrows i UpdateButtons jedynie przepisują dane z pola FStatus do obiektów przycisków — nie mają nic wspólnego z systemem, tak że nawet jeśli FConnected fałszywie zawiera prawdę, one same nie zamulą programu.

Sprawdziłem też inny kod podany na stronie MSDN:

JOYINFO joyinfo;
UINT wNumDevs, wDeviceID;
BOOL bDev1Attached, bDev2Attached;

if((wNumDevs = joyGetNumDevs()) == 0) return ERR_NODRIVER;

bDev1Attached = joyGetPos(JOYSTICKID1,&joyinfo) != JOYERR_UNPLUGGED;
bDev2Attached = wNumDevs == 2 && joyGetPos(JOYSTICKID2,&joyinfo) != JOYERR_UNPLUGGED;

if(bDev1Attached || bDev2Attached)   // decide which joystick to use
    wDeviceID = bDev1Attached ? JOYSTICKID1 : JOYSTICKID2;
else
    return ERR_NODEVICE;

i przepisałem go na Pascala, podłączając go pod obecny mechanizm:

var
  Info: JOYINFO;
  DevicesCount: Integer;
var
  ControllerID: Integer;
  Controller1Attached, Controller2Attached: Boolean;
begin
  DevicesCount := joyGetNumDevs();
  FConnected := DevicesCount > 0;

  if FConnected then
  begin
    Controller1Attached := joyGetPos(JOYSTICKID1, @Info) <> JOYERR_UNPLUGGED;
    Controller2Attached := (DevicesCount = 2) and (joyGetPos(JOYSTICKID2, @Info) <> JOYERR_UNPLUGGED);

    FConnected := Controller1Attached or Controller2Attached;

    if FConnected then
    begin
      if Controller1Attached then ControllerID := JOYSTICKID1;
      if Controller2Attached then ControllerID := JOYSTICKID2;

      FStatus := Default(JOYINFOEX);
      FStatus.dwSize := SizeOf(FStatus);
      FStatus.dwFlags := JOY_RETURNX or JOY_RETURNY or JOY_RETURNBUTTONS;

      joyGetPosEx(ControllerID, @FStatus);

      UpdateArrows();
      UpdateButtons();
    end
    else
      Reset();
  end
  else
    Reset();
end;

Niestety funkcja joyGetPos również ssie i tuż po starcie zwraca kod JOYERR_PARMS, a dopier0 później te prawidłowe. Summa summarum, obie funkcje nie ogarniają czy kontroler jest podłączony czy nie, tuż po starcie. Wszystko sprawdziłem pod debuggerem i jestem na 100% pewny, że po włączeniu programu kody błędów są nieprawidłowe.

Moje pytanie brzmi — w jaki sposób poprawnie (lub inaczej) sprawdzić, czy kontroler jest podłączony?

Wystarczy aby poprawnie określić czy jest podłączony i które ma ID, stan odczytam sobie funkcją joyGetPosEx, bo mi ona odpowiada. Zależy mi na tym, aby wybrany sposób potrafił rozpoznać dowolny, normalny kontroler (te od XBox i Play Station mam w dupie).

Ciekaw jestem dlaczego domyślnie te funkcje zwracają kod JOYERR_PARMS zamiast JOYERR_UNPLUGGED. :/

0

joyGetDevCaps też ssie. :|

Próbowałem się do tego dobrać w taki sposób, aby określić stan podłączenia kontrolera zanim wywołam joyGetPos lub joyGetPosEx, ale każda próba kończy się albo gigantycznymi lagami, albo nie wykrywaniem kontrolera w ogóle, nawet jeśli jest fizycznie podłączony i system go widzi.

3

Jeżeli czekanie na tę metodę powoduje lagi, to czemu nie sprawdzać tego, czy jest podłączony na innym wątku?

0

Żeby nie musieć bawić się w wątki i synchronizację. Lagów mogę równie dobrze uniknąć poprzez joySetCapture i przerzucanie komunikatów do obiektu zarządzającego stanem kontrolera, ale nie chcę mieszać w ten sposób — niech logika dotycząca kontrolera będzie siedzieć poza oknem.

Przy czym sama obsługa kontrolera nie sprawia żadnych problemów, bez względu na to czy jest podłączony czy nie. Problemem jest to, że jak włączę program bez podłączonego gamepada, to wtedy system nie potrafi poprawnie określić czy kontroler jest podłączony i bardzo długo zajmuje mu zwrócenie kodu błędu. Taka sytuacja ma miejsce tylko do pierwszego podłączenia — później już wszystko działa normalnie, mogę podłączać i odłączać do woli.

Tyle tylko, że jak kontrolera nie ma (od startu programu aż do końca sesji), to urządzeniem domyślnym jest klawiatura i program powinien normalnie działać. Nie może być tak, że podłączenie kontrolera (choćby na chwilę) ma mi framerate naprawiać. :D

Gdybym pisał normalną grę, to bym miał komunikaty dostarczone do okna i nie musiałbym ręcznie sprawdzać stanu gamepada. Ale robię zwykłą apkę okienkową, narzędzie do przetestowania kilku rzeczy i nie chcę całej logiki implementować w zdarzeniach formularza, czy w jakiś dziwny sposób przepychać danych z okna do klas.


Na razie sprawdzę czy joySetCapture rozwiąże problem i jeśli tak — przymknę oko na cudaczne przepychanie danych. Aplikacja jest jednowątkowa, więc bez problemu mogę nawet globalnych zmiennych użyć (fuj).

Edit: jednak nie, joySetCapture nie rozwiąże problemu, bo — oprócz dwóch osi — jest w stanie odczytać stan tylko czterech pierwszych przycisków. Co prawda właśnie cztery są mi potrzebne, bo czterech wymaga program (i mój kontroler ma cztery przyciski, więc dobrze się składa), ale chińskie głąby tak zmapowały kody przycisków, że dwa przyciski dają kody 1 i 2, a drugie dwa 9 i 10, a tych ta funkcja nie obejmuje. :|

2

a może sprawdzić urządzenia HID w systemie, np po kodzie producenta czy urządzenia. Można skorzystać z: https://github.com/LongDirtyAnimAlf/FPC-USB-HID

0

Jest to jakieś rozwiązanie i czytałem już wcześniej co nieco na ten temat, ale na razie nie mam ochoty się w to zagłębiać. Mimo wszystko dzięki za link i chęć pomocy. ;)

4

Problem rozwiązany. Najprostszym, perfekcyjnie działającym i wymagającym najmniej roboty rozwiązaniem, okazało się wydzielenie aktualizowania stanu kontrolera do osobnego wątku, co zasugerował @Pixello. Dzięki za podpowiedź. Nie wiem dlaczego sam na to nie wpadłem, ale jak zwykle najciemniej pod latarnią. Pewnie minęło by sporo czasu, zanim bym zatrybił i spróbował wątków. ;)

Teraz rozwiązanie. Początkowo metoda aktualizacji stanu kontrolera wyglądała tak:

procedure TDevice.Update();
begin
  FConnected := joyGetNumDevs() > 0;

  if FConnected then
  begin
    FStatus := Default(JOYINFOEX);
    FStatus.dwSize := SizeOf(FStatus);
    FStatus.dwFlags := JOY_RETURNX or JOY_RETURNY or JOY_RETURNBUTTONS;

    FConnected := joyGetPosEx(JOYSTICKID1, @FStatus) = JOYERR_NOERROR;

    if FConnected then
    begin
      UpdateArrows();
      UpdateButtons();
    end
    else
      Reset();
  end;
end;

Ten kod był wykonywany w ramach wątku głównego, bo cała logika narzędzia wykonywana jest w jednym, głównym wątku. Jeśli joyGetPosEx zasysał, to zamulał cały główny wątek, a więc framerate spadał. Skróciłem tę metodę do takiej postaci:

procedure TDevice.Update();
begin
  if FConnected then
  begin
    UpdateArrows();
    UpdateButtons();
  end
  else
    Reset();
end;

Nie ma tutaj pobierania stanu kontrolera, jest tylko przepisywanie danych w metodach Update* ze struktury JOYINFOEX do obiektów poszczególnych przycisków (lub resetowanie, gdy kontroler nie jest podłączony). Napisałem sobie klasę wątku aktualizatora stanu kontrolera — wygląda tak:

type
  TDeviceUpdater = class(TThread)
  private
    FDeviceStatus: PJOYINFOEX;
    FDeviceConnected: PBoolean;
  private
    FLocalStatus: JOYINFOEX;
    FLocalConnected: Boolean;
  private
    procedure UpdateDevice();
  public
    constructor Create(AStatus: PJOYINFOEX; AConnected: PBoolean);
  public
    procedure Execute(); override;
  end;

W konstruktorze pobieram wskaźniki na pola ze strukturą danych kontrolera oraz flagi określającej podłączenie.

constructor TDeviceUpdater.Create(AStatus: PJOYINFOEX; AConnected: PBoolean);
begin
  inherited Create(False);

  FDeviceStatus := AStatus;
  FDeviceConnected := AConnected;
end;

Kolejne dwa pola, oznaczone jako Local, są tymi, które uzupełnia funkcja joyGetPosEx. Jest to konieczne — joyGetPosEx nie może aktualizować bezpośrednio danych spod wskaźników, bo w tym samym czasie główny wątek może te dane odczytywać. Tutaj wymagana jest synchronizacja pomiędzy wątkami i taką synchronizację przeprowadza metoda UpdateDevice:

procedure TDeviceUpdater.UpdateDevice();
begin
  FDeviceStatus^ := FLocalStatus;
  FDeviceConnected^ := FLocalConnected;
end;

która jest wywoływana w ramach metody TThread.Synchronize. Główna metoda wątku, która pobiera stan kontrolera wygląda tak:

procedure TDeviceUpdater.Execute();
begin
  while not Terminated do
  begin
    FLocalStatus := Default(JOYINFOEX);
    FLocalStatus.dwSize := SizeOf(JOYINFOEX);
    FLocalStatus.dwFlags := JOY_RETURNX or JOY_RETURNY or JOY_RETURNBUTTONS;

    FLocalConnected := joyGetPosEx(JOYSTICKID1, @FLocalStatus) = JOYERR_NOERROR;

    Synchronize(@UpdateDevice);
    Sleep(10);
  end;
end;

Najpierw resetuje się i uzupełnia strukturkę, potem woła się joyGetPosEx, która uzupełnia lokalne pole z danymi o kontrolerze, a zwracany kod błędu określa stan flagi podłączenia. Potem zsynchronizowane przepisanie danych z pól wątku do pól wskazywanych przez pointery (prefiks Device, bo znajdują się w klasie TDevice) i oczekiwanie na kolejną aktualizację przez 10ms.

Tworzenie i zwalnianie wątku zdefiniowane jest w klasie TDevice:

procedure TDevice.InitUpdater();
begin
  FUpdater := TDeviceUpdater.Create(@FStatus, @FConnected);
  FUpdater.FreeOnTerminate := True;
end;

procedure TDevice.DoneUpdater();
begin
  FUpdater.Terminate();
  FUpdater.WaitFor();
end;

InitUpdater wołany jest w kontruktorze, po utworzeniu obiektów, do których przepisywane są dane ze struktury, a DoneUpdater w destruktorze TDevice, przed zwolnieniem z pamięci obiektów reprezentujących przyciski kontrolera.

I to tyle — żadna magia, a działa wyśmienicie. Jeszcze raz dzięki za pomoc. ;)

Pełny kod modułu tutaj: https://github.com/furious-programming/Fairtris/blob/master/source/Fairtris.Controller.pp

1 użytkowników online, w tym zalogowanych: 0, gości: 1