Programació de jocs en Android

La programació de videojocs és una de les parts més complexes a dins del món de la informàtica ja que requereix de molts coneixements tècnics que a més comprenen molts sectors. Per tenir-ne una idea bàsica, per crear un joc es necessita:

  • Definir el concepte del joc (equip creatiu)
  • Escriure el guió del joc (guionistes)
  • Dissenyar els personatges, escenaris i nivells (dissenyadors gràfics)
  • Crear els efectes sonors, música i veus (equip de so)
  • Programar la lògica del joc (programadors)

I a més, una vegada estigui tot llest, s’ha de comprovar que tot funcioni correctament per a finalment passar a la postproducció.

Com podem veure, la creació d’un videojoc no és trivial i implica el treball i la coordinació de moltes persones.

En aquesta unitat acabarem desenvolupant un joc, anirem fent poc a poc alguns dels passos descrits a la llista anterior, llavors, abans de posar-nos a programar, haurem de decidir la temàtica del nostre joc. Per fer això necessitarem conèixer alguns dels distints gèneres que existeixen:

  • Plataformes: un dels gèneres per excel·lència als jocs en 2 dimensions (2D), molt popular gràcies a jocs com Donkey Kong o Super Mario Bros (vegeu figura). En aquest gènere l’objectiu és el d’anar esquivant obstacles i en la majoria dels casos eliminar els enemics que ens anem trobant pel camí. Amb la popularització dels jocs en 3 dimensions (3D), els jocs de plataformes han disminuït respecte altres gèneres però sense arribar a desaparèixer.
Figura Joc de plataformes: Super Mario Bros

Un mod (videojocs) és una modificació d’un joc en el qual fent servir la mateixa base, es canvien els objectius i fins i tot la jugabilitat de l’original.

  • Tirs: jocs en el quals l’objectiu principal és atacar els enemics fent servir armes de foc. Entre els seus subgèneres més coneguts trobem:
    • En primer persona: en anglès First Person Shooter (FPS), són jocs en els quals es visualitza allò que el protagonista veuria a la realitat (perspectiva subjectiva). Aquest tipus de perspectiva combinada amb l’ús d’armes donen nom al gènere. Avui en dia és molt popular als jocs amb opció multijugador, és a dir, en aquells que els usuaris juguen entre ells i no contra personatges controlats per una intel·ligència artificial. Alguns dels jocs més coneguts d’aquest subgènere serien: Doom, Half Life i el seu mod Counter Strike o la saga Call of duty.
    • En tercera persona: en anglès Third Person Shooter (TPS), són molt similars en concepte als FPS a excepció que la càmera se situa a la part posterior del personatge, és a dir, veiem totalment o parcialment el cos d’aquest. Alguns exemples serien Tomb Raider, les sagues Resident Evil o el Gears of war.
    • Shoot ‘em up: jocs en els quals habitualment controlem un vehicle que s’enfronta amb una gran quantitat d’enemics a qui ha de destruir. En aquest subgènere és molt comú trobar perspectives zenitals o laterals i gràfics en 2D. Exemples de jocs d’aquest gènere serien: Thunder Force, R-Type o 1942.
  • Aventures gràfiques: molt populars durant els anys 90, tenen com a principal objectiu anar avançant en la història que ens proposen bé sigui mitjançant la resolució de trencaclosques, mitjançant diàlegs amb personatges o fent servir objectes. Entre les aventures gràfiques més populars trobem la saga Monkey Island, Broken Sword, The day of the tentacle o The longest journey entre altres.
  • Conducció/carreres: entraria a dins del gènere de simuladors. En aquest tipus de jocs, bé sigui amb una perspectiva en primera o tercera persona, el jugador condueix un vehicle per competir amb altres jugadors i arribar el primer a la meta. Alguns exemples d’aquest gènere serien la saga Need for speed, Gran Turismo o els distints Mario Kart.

Joc casual

Un joc casual és un joc en què la jugabilitat és molt senzilla, no necessita gaire temps de dedicació ni habilitats especials per jugar-lo. Ja existia abans de les plataformes mòbils però amb aquestes ha pres una gran importància.

Molts dels jocs presents avui en dia a les plataformes mòbils són considerats “jocs casuals”, aquest tipus de jocs tenen també un gènere associat. Exemples més evidents d’aquest tipus de jocs són els coneguts Angry Birds i Candy Crush Saga, tots dos tenen una mecànica simple i nivells molt curts que ens permetran jugar tot i no tenir molt de temps disponible, per exemple entre una parada i una altra del metro.

Conceptes bàsics de la programació de jocs

Abans de començar amb la programació de videojocs hi ha una sèrie de conceptes que caldria conèixer. Fem un repàs d’alguns que ens poden ser útils per entendre el funcionament dels jocs o bé que ens ajudaran en el seu procés de creació.

1. Píxels i resolució de pantalla

Totes les pantalles existents al mercat, incloses les dels telèfons mòbils i tauletes, estan dividides en píxels.

Un píxel és la menor unitat que podem representar en una pantalla. Una pantalla està formada per milions de píxels, dels quals necessitem conèixer-ne la posició respecte aquesta i el color que té.

La resolució d’una pantalla ens informarà de la quantitat de divisions horitzontals i verticals de què disposem, i això determinarà el número de píxels que tindrà. Per exemple, quan ens diuen que un telèfon mòbil té una resolució de 1080 x 1920 ens estan informant de la quantitat de divisions que té la pantalla en ample i alt respectivament (mirant el telèfon en portrait o les tauletes en landscape). Si multipliquem les dues divisions, ens donarà el total de píxels, amb la resolució de l’exemple anterior tindríem un total de 2073600 píxels.

Si a més tenim en compte les dimensions de pantalla, podem calcular la densitat de píxels (mesurada en ppi, píxels per inch) de la pantalla, és a dir, quants píxels hi ha per cada polzada de pantalla; a major densitat, més nítida serà la visualització de la pantalla. Per calcular la densitat de píxels hem d’aplicar la següent fórmula:

on és la resolució horitzontal, la vertical i la diagonal de la pantalla, és a dir, les polzades que té. Si sabem que la resolució de l’exemple anterior era d’un dispositiu amb una pantalla de 4,95”, la densitat de píxels seria:

.

2. Sprites i animacions

La profunditat de color fa referència al número de bits que dediquem a representar els colors, a major número de bits, més colors es podran fer servir. Per exemple, amb una profunditat de color de 16 bits es poden representar colors, és a dir, 65536 colors.

Un sprite és una imatge en mapa de bits d’un determinat ample i alt. De cada píxel que forma aquesta imatge cal guardar el color que tindrà així que l’espai ocupat per un sprite serà el total de píxels de la imatge multiplicat per la seva profunditat de color. Una imatge de 200×200 píxels amb una profunditat de color de 32 bits ocuparia 1.280.000 bits, que passat a bytes i posteriorment a kibibytes farien un total de 156,25 KiB.

  • Zoòtrop
  • Zoòtrop

Per tal de reduir la memòria que poden ocupar els sprites en ser redimensionats, es fa servir el concepte d’sprite sheet: un mapa de bits que conté totes les imatges que es faran servir a la nostra aplicació, del qual traurem tota aquella informació innecessària (com per exemple píxels transparents o colors que són innecessaris). Al final haurem d’aconseguir fer més lleugers els recursos gràfics amb la qual cosa obtindrem diverses millores: reduirem l’espai utilitzat per la nostra aplicació i en millorarem el rendiment. Al següent vídeo podeu veure un exemple d’aquest concepte:

El zoòtrop va ser un dispositiu que produïa la il·lusió de moviment i que és considerat un dels precursors del cinema. Aquest dispositiu era circular i podíem observar una animació creada a partir de fotogrames a través d’un visor vertical.

Per tal de realitzar animacions, necessitarem crear els sprites dels objectes dels diferents estats de l’animació, tal com es feia a un zoòtrop. Afegirem tots els sprites a l’sprite sheet i després els anirem canviant a la nostra aplicació per donar la sensació de moviment. Un exemple el trobem a la figura.

Figura Sprite sheet

3. Frame rate

El frame rate fa referència a la quantitat d’imatges (frames) que es mostren per segon. És una mesura de freqüència i la seva unitat són els frames per second (FPS).

4. Polling / Event handling

El terme polling fa referència a una consulta periòdica d’un valor, habitualment obtingut d’un dispositiu de hardware (un dispositiu d’entrada o un sensor, per exemple).

La gestió de l’entrada de l’usuari o l’estat dels sensors es pot fer mitjançant polling o mitjançant event handling (gestió d’esdeveniments).

  • Amb polling el que fem és comprovar l’estat actual d’un dispositiu d’entrada, per exemple en un bucle del joc comprovem si s’ha pitjat la pantalla o una tecla.
  • El sistema event handling ens permet gestionar la informació d’entrada a través d’events (esdeveniments, en català), és a dir, important, quan la necessitem conèixer, la seqüència dels events, per exemple, quan passem el dit per la pantalla ens interessa primer tractar el moment en què el dit toca la pantalla, després tractem el desplaçament del dit i finalment gestionem quan l’usuari aixeca el dit de la pantalla. Serien, per exemple, els events: touchDown, touchDragged i touchUp.

5. Gràfics 3D

Per tal de desenvolupar aplicacions que disposin de gràfics en tres dimensions haurem de recórrer a APIs gràfiques. Actualment les principals són:

  • OpenGL: Open Graphics Library és una API multiplataforma i multillenguatge per a la creació de gràfics en 2D i 3D. La versió 4.5 va ser publicada l’11 d’agost de 2014.
  • Direct3D: API gràfica de Microsoft a dins de les llibreries DirectX. Ens permet crear gràfics en 3D per a les plataformes i sistemes operatius de Microsoft: videoconsoles Xbox i sistemes operatius Windows (inclosos els sistemes operatius per a dispositius mòbils).

OpenGL ES (OpenGL for Embedded Systems) és una versió reduïda d’OpenGL dissenyada per a dispositius mòbils o consoles de videojocs. La numeració és distinta entre OpenGL i OpenGL ES.

Les versions d’Android Lollipop 5.X disposen de la versió d’OpenGL ES 3.1. Si observem el ”hello world” d’OpenGL que podem trobar a la pàgina de desenvolupadors http://developer.android.com/training/graphics/opengl/index.html podrem veure que la programació en 3D no és trivial i que requereix de molts coneixements i temps. Al tutorial enllaçat se’ns expliquen els passos per dibuixar figures geomètriques i per afegir-los moviment.

Entrada de dades

Un dels aspectes més importants dels videojocs és la manera amb què recollim la informació de l’usuari. A banda de la pantalla tàctil i dels teclats i botons físics, els dispositius mòbils acostumen a incorporar molts sensors que ens faciliten aquesta interacció i que estan dividits en tres categories:

  • Sensors de moviment: on trobem acceleròmetres, sensors de gravetat, giroscopis, etc.
  • Sensors d’entorn: baròmetres, termòmetres i sensors de llum.
  • Sensors de posició: sensors d’orientació i magnetòmetres.

Cadascun dels sensors ens podrà ser útil segons la finalitat de la nostra aplicació però sempre haurem de tenir en compte que no tots els dispositius incorporen tots els sensors. Android ens facilitarà l’accés a la informació sobre quins sensors tenim disponibles, les característiques de cadascun d’ells i l’obtenció de la informació que ens proporcionen a través d’un framework propi que forma part del package android.hardware.

Pantalla tàctil

La pantalla tàctil és el dispositiu d’entrada d’informació per excel·lència als dispositius mòbils. A través d’ella interactuem amb el dispositiu la major part del temps.

Des de la perspectiva de la programació d’aplicacions, per controlar què ha fet l’usuari, disposem del mètode onTouchEvent(MotionEvent event) de la classe Activity o bé del mètode onTouch(View v, MotionEvent event) de la interfície OnTouchListener. La diferència entre els dos és: mentre que al mètode onTouchEvent l’aplicació registrarà totes les pulsacions que es produeixin en la nostra activitat, independentment del view sobre el qual estem polsant, implementant la interfície onTouchListerner podrem especificar de quins views ens interessa conèixer les pulsacions, fent el corresponent setOnTouchListener.

Podeu descarregar el projecte de la següent aplicació a l’apartat “Touch Events” dels annexos.

Per entendre els events que es produeixen cada cop que l’usuari interactua amb la pantalla, crearem una aplicació, tot iniciant un nou projecte de nom TouchEvents i de domini cat.xtec.ioc.

El layout de la nostra aplicació tindrà una capsa de text que mostrarà els events que s’aniran generant (i que ens permetrà fer scroll), un LinearLayout que controlarà les accions de l’usuari amb la pantalla tàctil i una altra capsa de text que mostrarà la direcció en la qual l’usuari ha arrossegat el dit. Si posem aquests elements a dins d’un LinearLayout ens quedarà el següent codi:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="vertical"
  6. android:paddingBottom="@dimen/activity_vertical_margin"
  7. android:paddingLeft="@dimen/activity_horizontal_margin"
  8. android:paddingRight="@dimen/activity_horizontal_margin"
  9. android:paddingTop="@dimen/activity_vertical_margin"
  10. tools:context=".MainActivity">
  11.  
  12. <ScrollView
  13. android:id="@+id/scroll"
  14. android:layout_width="fill_parent"
  15. android:layout_height="fill_parent"
  16. android:layout_weight="1"
  17. android:fillViewport="true"
  18. android:scrollbars="vertical">
  19.  
  20. <TextView
  21. android:id="@+id/accions"
  22. android:layout_width="match_parent"
  23. android:layout_height="wrap_content"
  24. android:scrollbars="vertical"
  25. android:text=""
  26. android:textAppearance="?android:attr/textAppearanceSmall" />
  27. </ScrollView>
  28.  
  29. <LinearLayout
  30. android:id="@+id/linear"
  31. android:layout_width="match_parent"
  32. android:layout_height="fill_parent"
  33. android:layout_weight="1"
  34. android:background="#ff88eeff"
  35. android:orientation="vertical">
  36.  
  37.  
  38. <TextView
  39. android:layout_width="wrap_content"
  40. android:layout_height="wrap_content"
  41. android:textAppearance="?android:attr/textAppearanceSmall"
  42. android:text=""
  43. android:id="@+id/direccio" />
  44. </LinearLayout>
  45.  
  46. </LinearLayout>

Observeu que el textView d’identificador “accions” és a dins de l’element ScrollView. Això ens permetrà fer scroll en cas de produir-se overflow, és a dir, que el text no càpiga a l'espai assignat.

Cada cop que l'usuari faci una pulsació o arrossegui el dit pel LinearLayout d’identificador “linear”, es generaran una sèrie d'events que quedaran recollits al textView “accions”.

Abans de passar a la part del codi, modificarem l'entrada de menú settings per donar-li una utilitat. Aquesta netejarà el contingut del textView “accions”. Editem el fitxer /res/menu/menu_main.xml per deixar-lo de la següent manera:

  1. <menu xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:app="http://schemas.android.com/apk/res-auto"
  3. xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
  4. <item android:id="@+id/action_clear" android:title="@string/action_clear"
  5. android:orderInCategory="100" app:showAsAction = "ifRoom"/>
  6. </menu>

Hem fer tres modificacions de l'element item:

  • android:id=”@+id/action_settings” per android:id=”@+id/action_clear”: canviem el nom de l'identificador.
  • android:title=”@string/action_settings” per android:title=”@string/action_clear”: canviem el text que es mostrarà a la pantalla.
  • i app:showAsAction = “never” per app:showAsAction = “ifRoom”: fem que l'element de menú sigui visible a l'ActionBar sempre que hi hagi lloc disponible.

Com que hem canviat l'string que es mostrarà per pantalla, haurem d'incorporar-la al fitxer /res/values/strings.xml i podrem eliminar els recursos que no es facin servir, quedant de la següent manera:

  1. <resources>
  2. <string name="app_name">TouchEvents</string>
  3.  
  4. <string name="action_clear">Neteja</string>
  5. </resources>

Una vegada ja tenim les vistes i el menú preparats, donem funcionalitat a la nostra aplicació. Com que volem enregistrar les pulsacions a un view concret haurem d’implementar la interfície OnTouchListener i per això, haurem d’afegir el corresponent implements en la definició de la classe:

  1. public class MainActivity extends ActionBarActivity implements View.OnTouchListener {

Definirem les variables corresponents als views que volem modificar i afegirem dos float que guardaran l’inici d’un event d’arrossegar (drag): “iniciX” i “iniciY”. Per últim necessitarem un enter (“numAccio”) que actuarà com a comptador dels events que s’han generat.

Codi de definició de les variables:

  1. TextView accions, direccio;
  2. float iniciX, iniciY;
  3. int numAccio;

Al mètode onCreate, inicialitzarem les variables i afegirem l'OnTouchListener al textView d’accions, que quedarà de la següent manera:

  1. @Override
  2. protected void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setContentView(R.layout.activity_main);
  5.  
  6. // Afegim el "Listener" dels events de "touch" sobre el layout inferior.
  7. LinearLayout linear = (LinearLayout) findViewById(R.id.linear);
  8. linear.setOnTouchListener(this);
  9.  
  10. // Inicialitzem les variables
  11. accions = (TextView) findViewById(R.id.accions);
  12. direccio = (TextView) findViewById(R.id.direccio);
  13. iniciX = iniciY = 0;
  14. numAccio = 0;
  15.  
  16. }

Pel que fa al menú, deixem la funció onCreateOptionsMenu sense modificar i controlem si l’usuari ha fet clic a l’opció “Neteja” des de la funció onOptionsItemSelected, si l’usuari vol esborrar: netejarem el textView i posarem el comptador a 0:

  1. @Override
  2. public boolean onOptionsItemSelected(MenuItem item) {
  3. // Handle action bar item clicks here. The action bar will
  4. // automatically handle clicks on the Home/Up button, so long
  5. // as you specify a parent activity in AndroidManifest.xml.
  6. int id = item.getItemId();
  7.  
  8. //Netegem el TextView i posem el comptador a 0
  9. if (id == R.id.action_clear) {
  10. accions.setText("");
  11. numAccio = 0;
  12. return true;
  13. }
  14.  
  15. return super.onOptionsItemSelected(item);
  16. }

Tota la gestió de les pulsacions de l’usuari la farem a la funció onTouch(View v, MotionEvent event) en la qual disposem de dos arguments: la vista sobre la que s’ha fet una pulsació i el MotionEvent generat, que entre d’altra informació, ens permetrà saber quin event s’ha realitzat, sent alguns dels més importants els següents:

  • ACTION_DOWN: es produeix quan l’usuari inicia una pulsació, és a dir, al moment exacte que el dit fa contacte en la pantalla.
  • ACTION_UP: quan finalitzem la pulsació, quan el dit deixa de tocar la pantalla.
  • ACTION_MOVE: entre els events ACTION_DOWN i ACTION_UP hi ha hagut algun canvi en la posició del dit, si hi ha moviment és perquè l’usuari ha desplaçat el dit per la pantalla durant la pulsació.
  • ACTION_POINTER_DOWN: s’ha produït una pulsació d’un dit que no és el principal (passem a generar events multi touch). Quan aquest dit s’aixeca de la pantalla genera un ACTION_POINTER_UP.
  • ACTION_POINTER_INDEX_MASK: conjuntament amb ACTION_POINTER_INDEX_SHIFT ens permetrà saber quin índex té la pulsació en un event multi touch.

Per controlar un event d’arrossegament de dit farem servir els events ACTION_DOWN (on guardarem la posició inicial del dit a la pantalla) i ACTION_UP (que guardarà la posició final i ens permetrà calcular la direcció). Si volem saber quin desplaçament ha fet l’usuari, haurem de descartar tota la informació innecessària i calcular la trajectòria segons ens interessi; en 4-axis o en 8-axis (vegeu figura).

La coordenada (0,0) de la pantalla se situa a la part superior esquerra del dispositiu.

Figura Moviments en 4 i 8 axis
  • 4-axis: els valors possibles són dalt, dreta, baix i esquerra. Per saber la direcció en què l’usuari ha arrossegat el dit haurem de veure en quin eix (horitzontal o vertical) i en quina direcció ho ha fet. Com que és molt complex realitzar un desplaçament perfecte en un únic eix, haurem de descartar la informació que ens arriba de l’altre; una manera de fer-ho és detectar a quin eix s’ha produït un major desplaçament fent la resta entre els valors absoluts dels punts inicials i els punts finals. Per realitzar aquesta implementació haurem de guardar els punts X i Y inicials quan es produeix l’event ACTION_DOWN i recollir les posicions finals a l'ACTION_UP. Una vegada sabem l’eix, podrem obtenir fàcilment el sentit fent la resta entre el punt inicial i el final. Aquesta serà la implementació que farem a la nostra aplicació.
  • 8-axis: a més les direccions que disposem amb 4-axis, podem considerar les que es produeixen a partir d’una combinació dels dos eixos, és a dir, les posicions: dalt-dreta, baix-dreta, baix-esquerra i dalt-esquerra. Com que és molt complex realitzar un desplaçament de “0” en qualsevol eix, per tal d’implementar aquesta solució haurem de definir un valor mínim a partir del qual es considera que hi ha hagut un desplaçament en una determinada direcció. Si no afegim aquest valor mínim, ens trobaríem amb què tots els desplaçaments serien una combinació dels dos eixos i no hi hauria cap moviment dels 4 axis principals.

El codi de la funció onTouch serà el següent:

  1. @Override
  2. public boolean onTouch(View v, MotionEvent event) {
  3.  
  4. if (event.getAction() == MotionEvent.ACTION_DOWN) {
  5.  
  6. // Registrem l'event al TextView
  7. accions.setText(String.valueOf(numAccio) + " - " + "ACTION_DOWN\n" + accions.getText());
  8. iniciX = event.getX();
  9. iniciY = event.getY();
  10.  
  11.  
  12. } else if (event.getAction() == MotionEvent.ACTION_UP) {
  13.  
  14. // Registrem l'event al TextView
  15. accions.setText(String.valueOf(numAccio) + " - " + "ACTION_UP\n" + accions.getText());
  16. float finalX = event.getX();
  17. float finalY = event.getY();
  18.  
  19. //Comprovem quin dels dos moviments ha estat major. Si és l'horitzontal
  20. if (Math.abs(finalX - iniciX) > Math.abs(finalY - iniciY)) {
  21.  
  22. // Si la X final és major que la inicial ha fet "drag" cap a la dreta
  23. if (finalX > iniciX) {
  24. direccio.setText("→");
  25. // Si no, cap a l'esquerra
  26. } else {
  27. direccio.setText("←");
  28. }
  29. // Si és el vertical
  30. } else {
  31. // Si la Y final és major que la inicial ha fet "drag" cap avall
  32. if (finalY > iniciY) {
  33. direccio.setText("↓");
  34. // Si no, cap a dalt
  35. } else {
  36. direccio.setText("↑");
  37. }
  38. }
  39.  
  40. } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
  41.  
  42. // Registrem l'event al TextView
  43. accions.setText(String.valueOf(numAccio) + " - " + "ACTION_MOVE\n" + accions.getText());
  44.  
  45. }
  46. // Incrementem el comptador d'acció
  47. numAccio += 1;
  48. return true;
  49. }

Si l’acció realitzada (event.getAction()) ha estat ACTION_DOWN guardarem les coordenades X i Y de la pulsació i quan es produeixi ACTION_UP recollirem els valors finals.

Com que controlarem els moviments en 4-axis haurem de comprovar quin ha estat l’eix que més s’ha deplaçat amb la condició Math.abs(finalX - iniciX) > Math.abs(finalY - iniciY), és a dir, mirem quina és la diferència més gran entre els punts inicials i finals a l’eix de les X i l’eix de les Y. Només ens quedarà saber quina ha estat la direcció comparant els punts final i inicial de l’eix.

Acceleròmetre

L’acceleròmetre és uns dels sensors que més informació ens dóna sobre l’usuari. Ens permet conèixer en tot moment en quina posició es troba el mòbil, quins desplaçaments es produeixen i a quina velocitat. Aquest sensor és molt utilitzat als jocs (per controlar els girs o desplaçaments dels personatges) però també és utilitzat per a accions tan bàsiques com la de girar la pantalla quan l’usuari gira el telèfon. Podem controlar els canvis que es produeixen als eixos X, Y i Z tal com es pot veure a la figura

Figura Eixos X, Y i Z en un dispositiu mòbil

A la plataforma Android, els sensors s’han d’enregistrar per poder-se fer servir i els hem d’alliberar quan no siguin necessaris (per evitar consums excessius de bateria). Els mètodes onPause() i onResume() són els més adequats per realitzar aquestes tasques: quan s’executa l'onPause() deixem de seguir els canvis del sensor i continuem llegint-los en tornar a l’aplicació (onResume()).

Podeu descarregar el projecte de la següent aplicació a l’apartat “MotionSensor” dels annexos.

Per tal d’introduir els conceptes bàsics de la gestió de l’acceleròmetre desenvoluparem una aplicació que ens mostrarà en tot moment l’estat dels eixos. Creeu un projecte anomenat MotionSensor amb el Company Domain cat.xtec.ioc i creeu una activitat buida deixant els valors per defecte.

El layout de la nostra aplicació estarà format per sis TextView, tres seran les etiquetes “X”, “Y” i “Z” i els altres tres seran els valors dels sensors en cada moment.

El codi XML podria ser:

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
  3. android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
  4. android:paddingRight="@dimen/activity_horizontal_margin"
  5. android:paddingTop="@dimen/activity_vertical_margin"
  6. android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
  7.  
  8. <TextView android:text="X: " android:layout_width="wrap_content"
  9. android:layout_height="wrap_content"
  10. android:id="@+id/textX" />
  11.  
  12. <TextView
  13. android:layout_width="wrap_content"
  14. android:layout_height="wrap_content"
  15. android:textAppearance="?android:attr/textAppearanceSmall"
  16. android:id="@+id/valueX"
  17. android:layout_alignParentTop="true"
  18. android:layout_toRightOf="@+id/textX"
  19. android:layout_toEndOf="@+id/textX" />
  20.  
  21. <TextView
  22. android:layout_width="wrap_content"
  23. android:layout_height="wrap_content"
  24. android:textAppearance="?android:attr/textAppearanceSmall"
  25. android:text="Y: "
  26. android:id="@+id/textY"
  27. android:layout_below="@+id/textX"
  28. android:layout_alignParentLeft="true"
  29. android:layout_alignParentStart="true" />
  30.  
  31. <TextView
  32. android:layout_width="wrap_content"
  33. android:layout_height="wrap_content"
  34. android:textAppearance="?android:attr/textAppearanceSmall"
  35. android:id="@+id/valueY"
  36. android:layout_below="@+id/valueX"
  37. android:layout_alignLeft="@+id/valueX"
  38. android:layout_alignStart="@+id/valueX" />
  39.  
  40. <TextView
  41. android:layout_width="wrap_content"
  42. android:layout_height="wrap_content"
  43. android:textAppearance="?android:attr/textAppearanceSmall"
  44. android:text="Z: "
  45. android:id="@+id/textZ"
  46. android:layout_below="@+id/textY"
  47. android:layout_alignParentLeft="true"
  48. android:layout_alignParentStart="true" />
  49.  
  50. <TextView
  51. android:layout_width="wrap_content"
  52. android:layout_height="wrap_content"
  53. android:textAppearance="?android:attr/textAppearanceSmall"
  54. android:id="@+id/valueZ"
  55. android:layout_below="@+id/textY"
  56. android:layout_toRightOf="@+id/textY"
  57. android:layout_toEndOf="@+id/textY" />
  58.  
  59. </RelativeLayout>

Pel que fa al codi de l’aplicació, el primer que hem de fer per controlar els canvis al sensor és implementar la classe SensorEventListener afegint a la capçalera:

  1. public class MainActivity extends ActionBarActivity implements SensorEventListener {

Una vegada hem afegit l'implements, l’Android Studio ens informarà d’un error que solucionarem fent clic a Implement methods. Al final de la classe tindrem els mètodes:

  1. @Override
  2. public void onSensorChanged(SensorEvent sensorEvent) {
  3.  
  4. }
  5.  
  6. @Override
  7. public void onAccuracyChanged(Sensor sensor, int i) {
  8.  
  9. }

Aquests mètodes seran cridats quan es produeixin canvis als valors dels sensors (onSensorChanged) o en la seva precisió (onAccuracyChanged).

En el nostre cas ens interessen els valors dels sensors, ho sigui que haurem d’afegir el codi corresponent per recollir els canvis produïts. Prepararem, però, abans les variables i inicialitzarem el sensor.

Afegirem les següents declaracions de variables just entre la declaració de la classe i el mètode onCreate:

  1. TextView valueX, valueY, valueZ;
  2. SensorManager sensorMgr;
  3. Sensor sensor;

per després inicialitzar-les al mètode onCreate, els tres TextView ens permetran representar per pantalla l’estat dels sensors, el SensorManager ens permetrà assignar i enregistrar l’acceleròmetre, al qual podrem accedir a través de la variable Sensor.

Així, el contingut del mètode quedarà de la següent manera:

  1. @Override
  2. protected void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setContentView(R.layout.activity_main);
  5.  
  6. valueX = (TextView) findViewById(R.id.valueX);
  7. valueY = (TextView) findViewById(R.id.valueY);
  8. valueZ = (TextView) findViewById(R.id.valueZ);
  9.  
  10. sensorMgr = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
  11. sensor = sensorMgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  12. sensorMgr.registerListener(this, sensor , SensorManager.SENSOR_DELAY_NORMAL);
  13.  
  14. }

Per tal de deixar de controlar l’activitat dels sensors quan no estem a l’aplicació i evitar així els problemes de bateria descrits anteriorment, hem d’afegir el següent codi a la nostra activitat:

  1. @Override
  2. protected void onPause() {
  3. super.onPause();
  4. sensorMgr.unregisterListener(this);
  5. }
  6.  
  7. @Override
  8. protected void onResume() {
  9. super.onResume();
  10. sensorMgr.registerListener(this,sensor, SensorManager.SENSOR_DELAY_NORMAL);
  11. }

Quan la nostra activitat no estigui en primer pla (onPause()) deixem de seguir els canvis produïts al sensor (sensorMgr.unregisterListener(this);) i el tornem a enregistrar quan l’usuari torna a l’aplicació (onResume()).

Ja sols ens quedarà escriure el codi del mètode onSensorChanged(SensorEvent sensorEvent), que actualitzarà els TextView amb la informació que ens proporciona el sensor.

El paràmetre sensorEvent ens permetrà accedir al sensor, la precisió d’aquest, els valors, etc. Així, el primer que comprovarem és si la informació que estem rebent és la del sensor correcte amb el condicional:

  1. if(sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {

És a dir, accedim al sensor i fem la crida al mètode getType(), si és l’acceleròmetre, llavors actualitzarem els valors dels TextView. Per llegir els valors hem de recollir la variable values, un array que conté la informació de l’eix de les “X”, de les “Y” i de les “Z” a les posicions 0, 1 i 2 respectivament. Així, si consultem el values[0] obtindrem el valor de la “X”.

Una vegada comprovat que el sensor és el correcte, assignem als TextView els valors dels 3 eixos, passant-los prèviament a string.

  1. @Override
  2. public void onSensorChanged(SensorEvent sensorEvent) {
  3.  
  4. if(sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
  5.  
  6. valueX.setText(String.valueOf(sensorEvent.values[0]));
  7. valueY.setText(String.valueOf(sensorEvent.values[1]));
  8. valueZ.setText(String.valueOf(sensorEvent.values[2]));
  9.  
  10. }
  11.  
  12. }

Per tal de provar aquesta aplicació necessitarem fer-ho en un telèfon real ja que l’emulador d’Android no disposa d’aquesta opció. En cas de no tenir un dispositiu on provar-la, podeu mirar emuladors alternatius a l’oficial o instal·lar SensorSimulator i configurar-lo perquè treballi amb l’emulador.

El resultat serà el que podeu veure a la figura. Proveu a girar el telèfon en els distints eixos i observeu els canvis que es produeixen en els valors.

Figura Projecte MotionSensor en funcionament

Teclat / Botons

Cada vegada és menys freqüent trobar dispositius mòbils amb teclat físic, els teclats virtuals (com per exemple el que veiem a la figura) han millorat molt pel que fa a entrada de text i prediccions i han substituït pràcticament els teclats físics, que disposen de limitacions quant a funcionalitats i no es poden adaptar al context de les aplicacions tal com fan els teclats en pantalla. No obstant això, no hem de deixar de banda la gestió dels events de tecles, a banda del reduït però present sector de dispositius amb teclat incorporat, hem de tenir en compte aquells portàtils amb sistema operatiu Android i més pensant en l’actual tendència de convergència entre sistemes operatius d’escriptori i sistemes operatius pensats per a dispositius mòbils.

Figura Swype: exemple de teclat virtual en Android

Si volem gestionar aquest tipus d’events, de la mateixa manera que amb els de la pantalla tàctil, ho podem fer de dues maneres: gestionant qualsevol event que es produeixi a la nostra activitat o controlant aquells events dels views que nosaltres desitgem.

Gestió des de l'activitat

Per tal de gestionar els events de l’activitat, disposem de les funcions:

  • onKeyDown: cridat en el moment de polsar la tecla.
  • onKeyUp: cridat en finalitzar un onKeyDown, és a dir, quan deixem de pressionar la tecla.
  • onKeyLongPress: s’executa quan deixem pressionada una tecla.
  • onKeyMultiple: quan la mateixa tecla produeix diversos events down i up en un temps reduït.

Un exemple per controlar quan l’usuari prem la tecla de baixar volum seria el següent:

  1. @Override
  2. public boolean onKeyUp(int keyCode, KeyEvent event) {
  3.  
  4. if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
  5.  
  6. // Codi de baixar volum
  7. Toast.makeText(this, "Baixem volum!", Toast.LENGTH_SHORT).show();
  8. return true;
  9.  
  10. }
  11.  
  12. return super.onKeyUp(keyCode, event);
  13. }

El mètode onKeyUp s’executarà quan deixem de prémer la tecla de volum i rep com a paràmetres:

  • int keyCode: enter que fa referència a constants definides a la classe KeyEvent. Ens indica quina tecla ha estat premuda.
  • KeyEvent event: informació sobre l’event generat.

A l’exemple anterior comprovem que la tecla que s’ha pressionat és la corresponent a la variable KeyEvent.KEYCODE_VOLUME_DOWN, en cas de ser així controlem l’event i hem de retornar true en cas de voler gestionar-lo o false en cas de voler que sigui gestionat pel següent receptor.

Teniu una llista completa de les constants a http://developer.android.com/reference/android/view/KeyEvent.html així com també els mètodes i funcions disponibles de la classe KeyEvent.

Gestió d'un view en concret

Si la gestió dels events de tecles l’hem de fer des d’un view concret haurem de fer el corresponent implements de la classe:

  1. public class MainActivity extends ActionBarActivity implements View.OnKeyListener {

Com sempre ens donarà error i haurem de seleccionar el suggeriment de l’Android Studio i escollir l’opció Implement methods que ens afegirà el codi del mètode onKey(View view, int i, KeyEvent keyEvent). Els paràmetres són els mateixos que a la funció onKeyUp però a més ens informa del view que ha generat l’event amb el paràmetre View view.

Recordeu que per tal de controlar els events d’un view, a banda de fer l’implements hem d’assignar-li el listener amb vista.setOnKeyListener(this); on vista serà el view corresponent.

Motors de jocs

Els motors de jocs són frameworks dissenyats per a la creació de videojocs, és a dir, proporcionen recursos i metodologies per tal de facilitar-ne la programació. Un motor de joc ens ha de proporcionar una estructura sobre la qual començar a programar i crear la base del nostre joc d’una manera ràpida i senzilla. Aquest tipus de programari acostuma a ser modular, de manera que podem afegir aquelles funcionalitats que necessitem o bé programar-ne de noves sense afectar a la resta de components utilitzats. Alguns dels mòduls que podem trobar són: Tractament de físiques, tractament de col·lisions, intel·ligència artificial, gestió de recursos, etc.

En general, i sempre que sigui possible, es recomana la utilització d’un motor de joc, d’aquesta manera reduirem el temps de dedicació i els possibles errors de disseny que podria implicar crear un projecte des de l’inici.

El sector dels dispositius mòbils ha experimentat un fort creixement els darrers anys i els sector dels videojocs ha estat un dels més importants. Si analitzem els motors de jocs disponibles trobarem una gran varietat pel que fa a llicències, llenguatges de programació, funcionalitats, plataformes, etc. Fem un repàs d’alguns dels motors de jocs disponibles actualment:

  • Unity: desenvolupament de jocs multiplataforma (Android, iOS, Linux, OSX, PS4, Xbox One, etc.). Disposa d’una versió personal i una professional de pagament.
  • Unreal engine: motor multiplataforma molt popular de la companyia Epic Games. La primera versió del motor es va implementar als FPS: Unreal i Unreal Tournament. És gratuït però s’ha de pagar un royaltie d’un 5% en cas de comercialitzar el producte i obtenir beneficis.
  • AndEngine: motor conegut en el món dels jocs 2D en Android. És open source i gratuït.
  • LibGDX: ens permet crear jocs 2D i 3D multiplataforma per a: Windows, Linux, OSX, Android, iOS, BlackBerry i HTML5. És open source, gratuït i es programa amb el llenguage Java.
  • Godot Game Engine: motor multiplataforma i open source, disposa d’un llenguatge propi molt similar a python.

Per a la creació del joc en aquesta unitat hem decidit escollir LigGDX per les seves característiques i per tenir com a base el llenguatge de programació Java.

Anar a la pàgina anterior:
Referències
Anar a la pàgina següent:
Activitats