Een spelletje op de Spot
Dat is natuurlijk makkelijker gezegd dan gedaan. Okee, je zou de SPOT kunnen gebruiken als een soort game controller (ala Wii) en dan het eigenlijke spel op de PC laten draaien. Maar dat is meer iets voor een toekomstig hobby project. Het idee hier is echt om een spelletje te maken dat helemaal op de SPOT zelf draait.
Wat heb je om dingen te laten zien ? 8 Leds.
Wat heb je om mee te ineracteren ? 2 niet zo handig geplaatste knopjes en een.. acceleratie meter.
Dit vraagt bewegings gestuurde logische puzzel ! Maar wat is dat dan een logische puzzel. Nou, vroeger in de goede oude tijd van de C64 had je een spelletje dat heette 'V - The Game' naar de gelijknamige Science Fiction serie waar vriendelijk ogende aliens de wereld probeerden te beroven van al het water. En oja het waren stiekum reptielen. Nu ja, in dat spel zitten puzzels waarmee je deuren open moet krijgen door alle symbolen gelijk te maken. Zo iets dus.. alleen dan bewegings gestuurt dus niet bibberen ![]()
08 Juli 2007 : De acceleratie meter.
Het uitlezen van de accelerometer is gelukkig eenvoudig. Een kwestie van de instance opvragen en daaruit de 3 assen uitlezen. Voor het gemak heb ik er maar een array van gemaakt:
private IAccelerometer3D accel = EDemoBoard.getInstance().getAccelerometer(); private IScalarInput[] axis = new IScalarInput[] { accel.getXAxis(), accel.getYAxis(), accel.getZAxis()};
Dit was dus het makkelijke stuk, we kunnen de waarden uitlezen. Maar wat betekenen die waarden ? En hoe zet ik ze om naar commando's voor het spel ? Om te beginnen met het eerste, in een van de voorbeeld programma's in de SDK zitten interessante conversie waarden:
// default zero offset for raw accelerator value private double[] zeroOffset = {465.5,465.5,465.5}; // default conversion factor from raw accelerator value to G's private double[] sensitivity = {186.2,186.2,186.2};
En daarnaast de opmerking: The value for any particular SPOT may vary by as much as 10%. For more accurate results each SPOT can be calibrated to determine the zero offset and conversion factor for each axis. Jaja. Dus als we iets willen met de accelerometer moeten we dus eerst calibreren en daarna de waarden converteren naar iets bruikbaars.
Eerst maar calibreren. Als de puzzel wordt stil gehouden moet er niets gebeuren, dus dat is dan gelijk het calibatie punt voor de puzzel. Geen beweging is 0,0,0. Dan nu nog wat code dat zo iets produceert en de nul waarden opslaat in zeroOffset.
private void determineZeroPoint() { // accumulation of deviation double delta; // Holds the current values. double[] d=new double[3]; // Holds the previous values. double[] last=new double[3]; // Becomes true when the SPOT is stable. boolean stable=false; do try { // Read the accelerometer values. for (int t=0;t<3;t++) d[t]=axis[t].getValue(); // No deviation delta=0; // Deterine deviation with previous read. for (int t=0;t<3;t++) delta=delta+Math.abs(d[t]-last[t]); // If its very small, the SPOT is stable stable=delta<0.01; // Save the values for the next iteration. for (int t=0;t<3;t++) last[t]=d[t]; Utils.sleep(100); } catch (IOException ex) { stable=false; } while (!stable); // // Store the offset. // zeroOffset=d; }
Goed, de zero offset is gecalibreerd. Als de gevoeligheid ook zouden willen kalibreren dan wordt dat lastig want hoe doe je dat vanuit een rust positie ? Voor dit spel is het ook niet echt belangrijk zolang er maar een waarde uitkomt die enigsinds represantatief is. En 10% marge is dan wel te verwaarlozen (hoop ik
.
Dan volgt nu de volgende stap: het uitlezen van de gecorrigeerde waarden. Een kwestie van de waarde lezen, zijn offset er van af halen en delen door de gevoeligheids waarde :
private double[] getMovement(double[] d) { if (d==null) d=new double[3]; for (int t=0;t<3;t++) try { d[t]=(axis[t].getValue()-zeroOffset[t])/sensitivity[t]; } catch (IOException ex) { d[t]=0; } return d; }
Hoera, hiermee kunnen we de waarden uitlezen!
14 Juli 2007 : Hoe gaat die puzzel eigenlijk werken ?
Leuk natuurlijk dat we de accelerometer kunnen uitlezen maar als het doel is een puzzel ermee te maken moeten we toch eerst weten hoe die puzzel gaat werken voordat er bewegingen aan gekoppeld kunnen worden. Na wat gepuzzel over hoe de puzzel moet gaan werken ben ik tot het volgende idee gekomen : We geven de ledjes de keuren van de regenboog en husselen ze daarna door elkaar. Het doel van de puzzel is de kleuren weer terug in volgorde te krijgen. Van rood->oranje->geel->groen->cyaan->blauw en paars.
Er zijn 4 verschillende zetten die je kan doen. De puzzel naar links kantelen doet de ledjes naar links verschuiven. De puzzzel naar rechts kantelen doet het zelfde alleen dan naar rechts. En dan nog naar voor en naar achteren die ledjes verwisselen. Ach een plaatje zegt meer dan 1000 woorden :

De vraag is dan, hoe vertalen we de kantelingen van de spot naar operaties ? Eerst maar wat constanten defineeren.
private static final int ACTION_NONE=0; private static final int ACTION_SHIFTLEFT=1; private static final int ACTION_SHIFTRIGHT=2; private static final int ACTION_UP=3; private static final int ACTION_DOWN=4;
Daarnaast willen we ook kleine bewegingen onder een zekere limiet blokkeren (voor mensen die veel koffie drinken ![]()
private static final double LIMIT=0.2;
En dan is het een kwestie van kijken en vergelijken. Aangezien een operatie altijd over 1 bepaalde as (x,y of z) loopt is het handig om te weten of de andere assen in rust zijn (kleiner dan de limiet). Hiervoor declareren we 3 halt booleans, 1 voor iedere as. De operaties zijn nu eenvoudig te herkennen door te kijken naar de plus of min van de waarde, het overschijden van de limiet en dat de andere assen in rust zijn. Oftewel:
movement=getMovement(movement); // boolean xHalt=Math.abs(movement[0])<LIMIT; boolean yHalt=Math.abs(movement[1])<LIMIT; boolean zHalt=Math.abs(movement[2])<LIMIT; // boolean left=movement[0]<-LIMIT&&zHalt&&yHalt; boolean right=movement[0]>LIMIT&&zHalt&&yHalt; boolean up=movement[1]<-LIMIT&&zHalt&&xHalt; boolean down=movement[1]>LIMIT&&zHalt&&xHalt;
Nu nog de boel vertalen naar de constantes en we kunnen aan het echte werk beginnen ![]()
if (left) return ACTION_SHIFTLEFT; if (right) return ACTION_SHIFTRIGHT; if (up) return ACTION_UP; if (down) return ACTION_DOWN; // return ACTION_NONE;
Okee. Nu nog ff er een spel van maken.
Uiteraard moeten we beginnen met het caliberen van de accelerometer. Daarna moeten we de puzzel in zijn initieele stand zetten, plus een referentie kopie ervan maken zodat we weten wat de goede oplossing is (een Spot is ook maar een mens..) Dan maken we de speler duidelijk wat de goede oplossing is door de boel 5* te laten knipperen en dan gooien we de boel door de war en kan het spel beginnen. Oftewel in code :
protected void startApp() throws MIDletStateChangeException { determineZeroPoint(); setupPuzzle(puzzle); setupPuzzle(reference); blink(5); randomize(); playGame(); }
Het spelen van het eigenlijke spel is eigenlijk niet veel meer dan het gebruikelijke wat doet de speler, pas het spel aan, laat de resultaten zien en wacht ff. In bijna ieder spel is er zo'n soort main loop en ook de SpotPuzzle is geen uitzondering. Er is echter 1 sneaky detail en dat is namelijk dat het links en rechts schuiven operaties zijn die continu doorlopen, maar dat je bij een omhoog/omlaag operatie deze maar 1* wilt uitvoeren (anders heb je naast een prima motoriek ook een uitstekend gevoel voor timing nodig - en daar wordt het spel niet leuker van). Hiervoor gebruiken we de lastAction variabele. doUp/doDown worden alleen uitgevoerd als de voorgaande actie anders was dan de huidige.
private void playGame() { int lastAction=ACTION_NONE; while(true) { int action=getPlayerAction(); switch (action) { case ACTION_NONE: break; case ACTION_SHIFTLEFT : doShiftLeft();break; case ACTION_SHIFTRIGHT: doShiftRight();break; } if (action!=lastAction) switch (action) { case ACTION_UP : doUp();break; case ACTION_DOWN: doDown(); break; } updateLeds(); if (isSolved()) { blink(5); randomize(); } Utils.sleep(250); lastAction=action; } }
Het opzetten van de puzzel is eigenlijk niet meer dan het goed zetten van wat int[][]'s waarbij de eerste index overeenkomt met de led en de tweede index de rood,groen of blauw waarde aangeeft. Er worden twee arrays gebruikt, 1 om daadwerkelijk mee te puzzelen en de andere als vergelijkings materiaal om snel te kunnen of de puzzel is opgelost.
private int[][] puzzle=new int[leds.length][3]; private int[][] reference=new int[leds.length][3]; private void setupPuzzle(int[][] p) { p[0]=new int[] {255, 0, 0}; p[1]=new int[] {128, 64, 0}; p[2]=new int[] { 64,128, 0}; p[3]=new int[] { 0,255, 0}; p[4]=new int[] { 0,128, 64}; p[5]=new int[] { 0, 64,128}; p[6]=new int[] { 0, 0,255}; p[7]=new int[] {128, 0,128}; updateLeds(); }
Dan nu het door de war gooien van de puzzel. Gelukkig hebben we de java.util.Random klasse tot onze beschikking, dus is het genereren van pseudo random getallen geen probleem. Er is wel nog een andere subtiliteit, als we namelijk gewoon willekeurig de boel door de war gooien dan is het heel goed mogelijk dat een combinatie ontstaat die niet opgelost kan worden met de 4 bewegingen die we gedefineerd hebben. Dus moet de puzzel gehusseld worden door middel van de operaties die de speler ook kan uitvoeren. Nu ja, dan krijg je dus zo iets:
private void randomize() { Random r=new Random(System.currentTimeMillis()); for (int t=0;t<16;t++) { switch (Math.abs(r.nextInt())%4) { case 0: doShiftLeft();break; case 1: doUp();break; case 2: doDown();break; case 3: doShiftRight();break; } updateLeds(); } }
Het updaten van de leds is een triviale bezigheid met de demoboard API.
private ITriColorLED [] leds = EDemoBoard.getInstance().getLEDs(); private void updateLeds() { for (int t=0;t<puzzle.length;t++) { leds[t].setOn(); leds[t].setRGB(puzzle[t][0],puzzle[t][1],puzzle[t][2]); } Utils.sleep(50); }
Dan zijn er nog 2 dingen de overblijven: de daadwerkelijke bewegingen die de speler kan uitvoeren en het detecteren van een opgeloste puzzel. De omhoog en omlaag bewegingen kunnen eenvoudig worden uitgedrukt in het verwisselen van ledjes, dus daarvoor heb ik een geweldige swap methode gemaakt ![]()
public void doUp() { swap(2,4); swap(3,5); } public void doDown() { swap(3,4); swap(2,5); } protected void swap(int s,int d) { int[] tmp=puzzle[s]; puzzle[s]=puzzle[d]; puzzle[d]=tmp; }
De schuif operaties naar links en rechts lijken heel veel op het swappen van leds alleen dan over een array heen.
private void doShiftLeft() { int[] tmp=puzzle[0]; for (int t=0;t<puzzle.length-1;t++) puzzle[t]=puzzle[t+1]; puzzle[puzzle.length-1]=tmp; } private void doShiftRight() { int[] tmp=puzzle[puzzle.length-1]; for (int t=puzzle.length-1;t>0;t--) puzzle[t]=puzzle[t-1]; puzzle[0]=tmp; }
De opgelost detectie itereert over de puzzle[][] heen en vergelijkt de boel met de refentie array. Als alles overeenkomt dan is de puzzel opgelost !
private boolean isSolved() { for (int t=0;t<puzzle.length;t++) { for (int y=0;y<3;y++) { if (puzzle[t][y]!=reference[t][y]) return false; } } return true; }
Bekijk hoe de puzzel in het echt werkt op YouTube
Bekijk de Demo Video
en zie hoe Erik tot de conclusie komt dat puzzels maken leuker is dan ze oplossen.
Download de source code.
SPOTPuzzle.zip
: All source code for the project in a NetBeans 5.0 project structure.
SSPuzzle.rar
: The same only for the Orange SDK.
En dan nu, op naar het volgende avontuur.