Sviluppare un gioco per Android – SDL example

Introduzione

In questa puntata vedremo come compilare SDL 2.0 per Android e come eseguire una piccola demo sugli eventi touch. Questo non vuol dire che tutto il lavoro successivo sarà localizzato intorno a lei, piuttosto è solo un esempio per farvi scoprire le potenzialità dell’NDK utilizzando una famosa libreria per scrivere giochi e/o programmi multipiattaforma.

Devo sottolineare il fatto che la libreria utilizzata in questo esempio è ancora sotto intenso sviluppo e non completa (sopratutto per quanto riguarda la nuova documentazione), ma visto che molte funzioni sono simili alla versione 1.2 credo che chi l’ha già utilizzata si troverà a proprio agio. Oltre a questo, chi già avesse seguito gli articoli su pygame, si ritroverà molti dei concetti logici affrontati in quei momenti, visto che si parlava comunque di SDL (ne approfitto per segnalarvi che è stato effettuato anche il porting su Android : http://pygame.renpy.org/).

SDL non è comunque un prodotto troppo acerbo per essere provato, guardate per esempio Negative Space per Android o Eufloria per Windows e iOS; per essere una libreria tutt’ora in costruzione questi due primi prodotti non sembrano niente male (sopratutto Eufloria), quindi penso valga la pena spendere qualche parola, così chi è veramente interessato potrà approfondire a sua discrezione.

La libreria di base comprende la gestione dello schermo, degli eventi e funzioni base per renderizzare sul video oltre ad alcune per gestire il timer.

SDL 2.0

Per prima cosa, controllate di aver già installato mercurial nel vostro pc, così da poter scaricare la libreria dove preferite attraverso il comando apposito da terminale (o cmd per gli utenti windows):

hg clone http://hg.libsdl.org/SDL

Una volta sincronizzata la cartella della libreria procedete come segue :

  1. Copiate la cartella nominata android-project dove preferite. Rinominate la cartella a vostro piacimento, visto che sarà il nuovo progetto.
  2. All’interno del vostro nuovo progetto copiate tutta la cartella SDL scaricata con mercurial in nomeprogetto/jni.
  3. Sostituite dentro la cartella nomeprogetto/jni/SDL/include il file SDL_config.h con SDL_config_android.h . Per farlo potete anche rinominare i file o copiare il contenuto di SDL_config_android.h all’interno di SDL_config.h .
  4. Modificate il file local.properties nella cartella del progetto aggiungendo la cartella di installazione del vostro SDK di Android :
    sdk.dir=C:\\Android\\android-sdk
  5. Nella cartella jni/src del progetto create un file di nome main.cpp e modificate l’Android.mk presente in questa cartella aggiungendolo nei LOCAL_SRC_FILES al posto di YourSourceHere.c e di $(SDL_PATH)/src/main/android/SDL_android_main.cpp . Riempite il file main.cpp con il codice sottostante.
  6. Aggiornate il progetto da linea di comando dopo aver cancellato il file build.xml :
    android update project -t android-10 -p .
  7. Successivamente, dopo aver copiato il main che trovate di seguito (se non lo avete ancora fatto), eseguite ndk-build e ant debug install per installarlo sul vostro device o sull’emulatore che dovete rispettivamente collegare o avviare.

Ecco il codice dell’esempio in cpp. Questo non è niente altro che il testgesture.c già presente tra gli esempi SDL nella cartella test, modificato in piccole parti.

#ifdef __ANDROID__
#include <jni.h>
#include <SDL.h>
#include "SDL_touch.h"
#include "SDL_gesture.h"
#include <time.h>
#include <android/log.h>

/* Make sure we have good macros for printing 32 and 64 bit values */
#ifndef PRIs32
#define PRIs32 "d"
#endif
#ifndef PRIu32
#define PRIu32 "u"
#endif
#ifndef PRIs64
#ifdef __WIN32__
#define PRIs64 "I64"
#else
#define PRIs64 "lld"
#endif
#endif
#ifndef PRIu64
#ifdef __WIN32__
#define PRIu64 "I64u"
#else
#define PRIu64 "llu"
#endif
#endif

#define WIDTH 640
#define HEIGHT 480
#define BPP 4
#define DEPTH 32

//MUST BE A POWER OF 2!
#define EVENT_BUF_SIZE 256

#define VERBOSE 0

static SDL_Window *window;
static SDL_Event events[EVENT_BUF_SIZE];
static int eventWrite;
static int colors[7] = {0xFF,0xFF00,0xFF0000,0xFFFF00,0x00FFFF,0xFF00FF,0xFFFFFF};

typedef struct {
  float x,y;
} Point;

typedef struct {
  float ang,r;
  Point p;
} Knob;

static Knob knob;

void handler (int sig)
{
  SDL_Log ("exiting...(%d)", sig);
  exit (0);
}

void perror_exit (char *error)
{
  perror (error);
  handler (9);
}

void setpix(SDL_Surface *screen, float _x, float _y, unsigned int col)
{
  Uint32 *pixmem32;
  Uint32 colour;
  Uint8 r,g,b;
  int x = (int)_x;
  int y = (int)_y;
  float a;

  if(x < 0 || x > screen->w) return;
  if(y < 0 || y > screen->h) return;

  pixmem32 = (Uint32*) screen->pixels  + y*screen->pitch/BPP + x;

  SDL_memcpy(&colour,pixmem32,screen->format->BytesPerPixel);

  SDL_GetRGB(colour,screen->format,&r,&g,&b);
  //r = 0;g = 0; b = 0;
  a = (float)((col>>24)&0xFF);
  if(a == 0) a = 0xFF; //Hack, to make things easier.
  a /= 0xFF;
  r = (Uint8)(r*(1-a) + ((col>>16)&0xFF)*(a));
  g = (Uint8)(g*(1-a) + ((col>> 8)&0xFF)*(a));
  b = (Uint8)(b*(1-a) + ((col>> 0)&0xFF)*(a));
  colour = SDL_MapRGB( screen->format,r, g, b);

  *pixmem32 = colour;
}

void drawLine(SDL_Surface *screen,float x0,float y0,float x1,float y1,unsigned int col) {
  float t;
  for(t=0;t<1;t+=(float)(1.f/SDL_max(SDL_fabs(x0-x1),SDL_fabs(y0-y1))))
    setpix(screen,x1+t*(x0-x1),y1+t*(y0-y1),col);
}

void drawCircle(SDL_Surface* screen,float x,float y,float r,unsigned int c)
{
  float tx,ty;
  float xr;
  for(ty = (float)-SDL_fabs(r);ty <= (float)SDL_fabs((int)r);ty++) {
    xr = (float)sqrt(r*r - ty*ty);
    if(r > 0) { //r > 0 ==> filled circle
      for(tx=-xr+.5f;tx<=xr-.5;tx++) {
	setpix(screen,x+tx,y+ty,c);
      }
    }
    else {
      setpix(screen,x-xr+.5f,y+ty,c);
      setpix(screen,x+xr-.5f,y+ty,c);
    }
  }
}

void drawKnob(SDL_Surface* screen,Knob k) {
  drawCircle(screen,k.p.x*screen->w,k.p.y*screen->h,k.r*screen->w,0xFFFFFF);
  drawCircle(screen,(k.p.x+k.r/2*cosf(k.ang))*screen->w,
  	            (k.p.y+k.r/2*sinf(k.ang))*screen->h,k.r/4*screen->w,0);
}

void DrawScreen(SDL_Surface* screen)
{
  int i;
#if 1
  SDL_FillRect(screen, NULL, 0);
#else
  int x, y;
  for(y = 0;y < screen->h;y++)
    for(x = 0;x < screen->w;x++)
	setpix(screen,(float)x,(float)y,((x%255)<<16) + ((y%255)<<8) + (x+y)%255);
#endif

  //draw Touch History
  for(i = eventWrite; i < eventWrite+EVENT_BUF_SIZE; ++i) {
    const SDL_Event *event = &events[i&(EVENT_BUF_SIZE-1)];
    float age = (float)(i - eventWrite) / EVENT_BUF_SIZE;
	float x, y;
	unsigned int c, col;

    if(event->type == SDL_FINGERMOTION ||
       event->type == SDL_FINGERDOWN ||
       event->type == SDL_FINGERUP) {

      SDL_Touch* inTouch = SDL_GetTouch(event->tfinger.touchId);
      if(inTouch == NULL) continue;

      x = ((float)event->tfinger.x)/inTouch->xres;
      y = ((float)event->tfinger.y)/inTouch->yres;

      //draw the touch:
      c = colors[event->tfinger.fingerId%7];
      col = ((unsigned int)(c*(.1+.85))) | (unsigned int)(0xFF*age)<<24;
		SDL_Rect rect;
		rect.w = 40 ;
		rect.h = 40 ;
		rect.x = (x*screen->w)-20 ;
		rect.y = (y*screen->h)-20 ;
		SDL_FillRect(screen,&rect,col);
    }
  }

  if(knob.p.x > 0)
    drawKnob(screen,knob);

  SDL_UpdateWindowSurface(window);
}

SDL_Surface* initScreen(int width,int height)
{
  if (!window) {
    window = SDL_CreateWindow("Gesture Test",
                              SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
                              WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE);
  }
  if (!window) {
    return NULL;
  }
  return SDL_GetWindowSurface(window);
}

int main(int argc, char* argv[])
{
  SDL_Surface *screen;
  SDL_Event event;
  SDL_bool quitting = SDL_FALSE;
  SDL_RWops *src;

  //gesture variables
  knob.r = .1f;
  knob.ang = 0;

  if (SDL_Init(SDL_INIT_VIDEO) < 0 ) return 1;

  if (!(screen = initScreen(WIDTH,HEIGHT)))
    {
      SDL_Quit();
      return 1;
    }

  while(!quitting) {
    while(SDL_PollEvent(&event))
      {
	//Record _all_ events
	events[eventWrite & (EVENT_BUF_SIZE-1)] = event;
	eventWrite++;

	switch (event.type)
	  {
	  case SDL_QUIT:
	    quitting = SDL_TRUE;
	    break;
	  case SDL_KEYDOWN:
	    switch (event.key.keysym.sym)
	      {
	      case SDLK_SPACE:
		SDL_RecordGesture(-1);
		break;
	      case SDLK_s:
		src = SDL_RWFromFile("gestureSave","w");
		SDL_Log("Wrote %i templates",SDL_SaveAllDollarTemplates(src));
		SDL_RWclose(src);
		break;
	      case SDLK_l:
		src = SDL_RWFromFile("gestureSave","r");
		SDL_Log("Loaded: %i",SDL_LoadDollarTemplates(-1,src));
		SDL_RWclose(src);
		break;
	      case SDLK_ESCAPE:
		quitting = SDL_TRUE;
		break;
	    }
	    break;
	  case SDL_WINDOWEVENT:
            if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
	      if (!(screen = initScreen(0, 0)))
	      {
		SDL_Quit();
		return 1;
	      }
            }
	    break;
	  case SDL_FINGERMOTION:
#if VERBOSE
	    SDL_Log("Finger: %i,x: %i, y: %i",event.tfinger.fingerId,
	    	   event.tfinger.x,event.tfinger.y);
#endif
		{
			SDL_Touch* inTouch = SDL_GetTouch(event.tfinger.touchId);
			SDL_Finger* inFinger = SDL_GetFinger(inTouch,event.tfinger.fingerId);
		}
	    break;
	  case SDL_FINGERDOWN:
#if VERBOSE
	    SDL_Log("Finger: %"PRIs64" down - x: %i, y: %i",
		   event.tfinger.fingerId,event.tfinger.x,event.tfinger.y);
#endif
	    break;
	  case SDL_FINGERUP:
#if VERBOSE
	    SDL_Log("Finger: %"PRIs64" up - x: %i, y: %i",
	    	   event.tfinger.fingerId,event.tfinger.x,event.tfinger.y);
#endif
	    break;
	  case SDL_MULTIGESTURE:
#if VERBOSE
	    SDL_Log("Multi Gesture: x = %f, y = %f, dAng = %f, dR = %f",
		   event.mgesture.x,
		   event.mgesture.y,
		   event.mgesture.dTheta,
		   event.mgesture.dDist);
	    SDL_Log("MG: numDownTouch = %i",event.mgesture.numFingers);
#endif
	    knob.p.x = event.mgesture.x;
	    knob.p.y = event.mgesture.y;
	    knob.ang += event.mgesture.dTheta;
	    knob.r += event.mgesture.dDist;
	    break;
	  case SDL_DOLLARGESTURE:
	    SDL_Log("Gesture %"PRIs64" performed, error: %f",
		   event.dgesture.gestureId,
		   event.dgesture.error);
	    break;
	  case SDL_DOLLARRECORD:
	    SDL_Log("Recorded gesture: %"PRIs64"",event.dgesture.gestureId);
	    break;
	  }
      }
    DrawScreen(screen);
  }
  SDL_Quit();
  return 0;
}

// Called before SDL_main() to initialize JNI bindings in SDL library
extern "C" void SDL_Android_Init(JNIEnv* env, jclass cls);

// Library init
extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    return JNI_VERSION_1_4;
}

// Start up the SDL app
extern "C" int Java_org_libsdl_app_SDLActivity_nativeInit(JNIEnv* env, jclass cls, jobject obj)
{
    /* This interface could expand with ABI negotiation, calbacks, etc. */
    SDL_Android_Init(env, cls);

	main(0,NULL);

}

#endif /* __ANDROID__ */

Osservazioni

Da notare le ultime fuzioni che estendono quelle già presenti nel sorgente java di SDL che si trova in nomeprogettosrc/org/libsdl/app. In particolare nell’ultima viene fatta l’inizializzazione della libreria tramite il comando SDL_Android_Init . Da quel momento in poi, è possibile utilizzare la libreria SDL in tutte le sue parti (alcune librerie sono esterne e vanno aggiunte al progetto e sono sdl image,mixer,net,rtf e ttf. Per questo esempio non sono necessarie) senza modificare il nostro codice a seconda della piattaforma (il processo di init della libreria può essere differente a seconda dell’OS utilizzato).

Il funzionamento del programma è molto semplice : si collezionano gli eventi che a noi interessano (SDL_FINGERDOWN, SDL_FINGERUP, SDL_MULTIGESTURE, SDL_FINGERMOTION) in un buffer (events) che verrà successivamente analizzato durante la fase di rendering (funzione DrawScreen). A seconda della lunghezza del buffer utilizzata vedremo sullo schermo la storia degli eventi da noi creati, che sono rappresentati creando dei quadrati sul punto in cui è stato effettuato il tocco dello schermo.

Questo test prevede anche l’utilizzo del multigesture, che in questo caso fa ingrandire e rimpicciolire una “figura” a forma di occhio a seconda dei nostri gesti con due dita.

Come potete vedere la struttura del loop è identica a quelle viste nelle guide su pygame, con un ciclo principale ed un altro annidato per gli eventi con i dovuti accorgimenti per gestire i vari input (ricordate che questo file viene utilizzato come test delle librerie, non si sta implementando niente di particolare) .

Conclusioni

Con questa base è possibile aggiungere features come il supporto per openGL ES ed il rendering accelerato. Se avete dei progetti già scritti in SDL potreste provare a riportarli su questo nuovo dispositivo.

Come ho già detto l’articolo è solo a titolo dimostrativo, non approfondirò al momento i molti altri aspetti inerenti a questa libreria. Piuttosto mi premeva il fatto di lasciarvi qualcosa di nuovo tra le mani, anche se è molto difficile andare oltre a semplici dimostrazioni per questioni di tempo.

Alcuni titoli come Doom o Return to castle Wolfenstein per Android utilizzano sempre SDL e visto il recente interesse per videogiochi multipiattaforma credo che sarà sempre più interessante imparare ad utilizzare una libreria del genere.

Sarei curioso di conoscere i vostri pareri riguardo SDL e i traguardi da essa raggiunti. Vi aspetto dunque nei commenti e nei prossimi articoli.

Press ESC to close