Wirtualna kierownica do gier w oparciu o Kinecta

Mając chwilę czasu zastanawiam się ostatnio po jaką zabawkę sięgnąć. Ostatnim razem pokazywałem jak z wykorzystaniem Arduino i paska LED można stworzyć własny system Ambilightdo monitora, dzisiaj postanowiłem powrócić jednak do zabawy z Kinectem. Jakiś czas temu pokazałem jak skonfigurować sensor do pracy z Javą wykorzystując bibliotekę J4K i jak w prosty sposób można sterować kursorem myszy, teraz postanowiłem stworzyć wirtualną kierownicę, która pozwoli kontrolować np. samochody w grach.

Jak to działa

Założenie jest dosyć proste. Za pomocą Kinect SDK możemy odczytać położenie dłoni, łokci, czy nóg względem osi XY, która wyznaczana jest mniej więcej przez nas kręgosłup i linię przechodzącą przez nasz brzuch. Dodatkowo biblioteka J4K umożliwia konfigurację odczytu jedynie górnej partii ciała, dzięki czemu możemy stworzyć aplikację działającą w trybie siedzącym. Kierownica w grach działa najczęściej w banalny sposób i sterowanie odbywa się poprzez wciskanie strzałek w prawo, bądź lewo. Odczytując położenie dłoni możemy więc sprawdzić, która dłoń jest wyżej, a która niżej i na tej podstawie stwierdzić, w którym kierunku chce skręcić użytkownik. Jeśli dłonie są mniej więcej na jednym poziomie zakładamy, że użytkownik jedzie prosto. Idąc nieco dalej można by się pokusić o sterowanie bardziej analogowe, czyli wykrywanie tego, czy chcemy skręcać łagodnie, czy agresywnie, ale na ten moment sobie to odpuścimy.

kierownica *fot: flickr.com/photos/jamiemc/2845804445/*
  • Jeśli lewa ręka jest w ćwiartce 1 a prawa w 4 - skręcamy w prawo.
  • Jeśli lewa ręka jest w ćwiartce 3 a prawa w 2 - skręcamy w lewo.
  • Jeśli ręce znajdują się w martwej strefie pomiędzy ćwiartkami - jedziemy prosto.

Najprostszym sposobem na wykrycie odpowiedniej sytuacji jest wyliczenie różnicy pomiędzy współrzędną Y obu dłoni. W przypadku jazdy prosto można się spodziewać, że różnica ta będzie niewielka, mniej więcej w zakresie (-0.1, 0.1), a przy większej będzie to skręt w jedną ze stron.

Projekt

To jak skonfigurować projekt, aby działał on z Kinectem pokazywałem już poprzednim razem, więc nie będę tego powtarzał. Sam kod aplikacji jest stosunkowo prosty, co widać poniżej

package pl.javastart.kinect;

import java.awt.AWTException;
import java.awt.Robot;
import java.awt.event.KeyEvent;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import edu.ufl.digitalworlds.j4k.J4KSDK;
import edu.ufl.digitalworlds.j4k.Skeleton;

public class KinectWheel extends J4KSDK {
	public static final double TURN_DIFFERENCE = 0.1;
	public static final int TURN_LEFT = -1;
	public static final int TURN_RIGHT = 1;
	public static final int GO_STRAIGHT = 0;
	
	private AtomicInteger direction = new AtomicInteger(GO_STRAIGHT);
	private Robot robot;
	private DirectionChanger directionChanger;

	public KinectWheel() {
		configureRobot();
		trackDirection();
	}

	private void configureRobot() {
		try {
			robot = new Robot();
		} catch (AWTException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Przyciski wciskamy w osobnym wątku
	 */
	private void trackDirection() {
		directionChanger = new DirectionChanger();
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					directionChanger.execute();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
	}

	@Override
	public void onColorFrameEvent(byte[] arg0) {
	}

	@Override
	public void onDepthFrameEvent(short[] arg0, byte[] arg1, float[] arg2,
			float[] arg3) {
	}

	@Override
	public void onSkeletonFrameEvent(boolean[] skeletons, float[] positions,
			float[] orientations, byte[] state) {
		Skeleton skeleton = null;
		for (int i = 0; i < skeletons.length; i++) {
			if (skeletons[i]) {
				skeleton = Skeleton.getSkeleton(i, skeletons, positions, orientations, state, this);
			}
		}
		if (skeleton != null) {
			calculateHandDiff(skeleton);
		}
	}

	/**
	 * Wyliczamy różnicę wysokości dłoni i zmieniamy kierunek
	 */
	private void calculateHandDiff(Skeleton skeleton) {
		float rightHandY = skeleton.get3DJointY(Skeleton.HAND_RIGHT);
		float leftHandY = skeleton.get3DJointY(Skeleton.HAND_LEFT);
		float handDiff = rightHandY - leftHandY;
		changeDirection(handDiff);
		checkStopCondition(rightHandY, leftHandY);
	}
	
	private void changeDirection(float diff) {
		if (diff > TURN_DIFFERENCE) {
			direction.set(TURN_LEFT);
		} else if (diff < -TURN_DIFFERENCE) {
			direction.set(TURN_RIGHT);
		} else {
			direction.set(GO_STRAIGHT);
		}
	}
	
	/**
	 * Jeśli obie ręce uniesione są wysoko kończymy działanie aplikacji
	 */
	private void checkStopCondition(float left, float right) {
		final float stopCondition = 0.15f;
		if(left > stopCondition && right > stopCondition) {
			directionChanger.stop();
		}
	}

	public static void main(String[] args) {
		System.out.println("Kinect virtual wheel");
		KinectWheel kinect = new KinectWheel();
		kinect.start(Kinect.DEPTH | Kinect.COLOR | Kinect.SKELETON | Kinect.XYZ
				| Kinect.PLAYER_INDEX);
		kinect.setNearMode(true);
		kinect.setSeatedSkeletonTracking(true);
		System.out.println("Bye bye");
	}

	/**
	 * Symulacja ciągłego wciskania przycisku
	 */
	private class DirectionChanger {

		private AtomicBoolean running = new AtomicBoolean(true);

		public void execute() throws InterruptedException {
			while (running.get()) {
				switch (direction.get()) {
				case GO_STRAIGHT:
					robot.keyRelease(KeyEvent.VK_D);
					robot.keyRelease(KeyEvent.VK_A);
					break;
				case TURN_RIGHT:
					robot.keyRelease(KeyEvent.VK_A);
					robot.keyPress(KeyEvent.VK_D);
					break;
				case TURN_LEFT:
					robot.keyRelease(KeyEvent.VK_D);
					robot.keyPress(KeyEvent.VK_A);
				}
				Thread.sleep(10);
			}
		}

		public void stop() {
			running.set(false);
		}
	}

}

Aplikacja oparta jest o dwa wątki. W wątku głównym odczytujemy położenie dłoni i odpowiednio ustawiamy wartość direction . W drugim wątku widzimy pętlę, która odpowiada za symulowanie wciskania klawiszy A lub D (najczęściej lewo lub prawo w grach) używając klasy Robot. Dodatkowo stworzona została prosta metoda checkStopCondition() , która sprawi, że aplikacja się wyłączy, gdy obie ręce uniesiemy ku górze. W metodzie main() dodaliśmy również konfigurację do obiektu J4K, gdzie poprzez metody setNearMode() i setSeatedSkeleton() wskazujemy, że siedzimy blisko sensora i śledzona ma być górna część ciała.

Prezentacja

Poniżej znajduje się krótki film obrazujący działanie powyższej aplikacji. Jak widać nie jest ona może idealna, głównie z powodu niedokładności odczytu współrzędnych, ale radość z wirtualnego kontrolera jest niemała :)

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.