offline
- Srki_82

- Moderator foruma
- Srđan Tot
- Am I evil? I am man, yes I am.
- Pridružio: 12 Jul 2005
- Poruke: 2483
- Gde živiš: Ljubljana
|
Konačno i nastavak teme FPC za Android... kako napisati aplikaciju koju, uz minimalno napora, možemo iskompajlirati za Windows, Linux, Mac OS X i Android (nažalost, za iPad/iPhone je potrebno na poseban način iskompajlirati FPC pa ću o tome pisati neki drugi put). Pre nego što počnete da čitate ovo, bilo bi dobro da već znate da pravite jednostavne igre za Android i bar jedan desktop OS. Ako želite da iskompajlirate program za Android, treba vam i najnoviji Android SDK, Android NDK i Eclipse (minimalna verzija Androida za koju će raditi OpenGL aplikacije kompajlirane iz FPC je 1.6).
Za početak moramo obratiti pažnju na sve što se radi drugačije na različitim platformama. Svaka platforma ima svoj sistem za kreiranje prozora, za obradu poruka i crtanje. Ono što će biti isto za sve platforme je logika koja će upravljati tokom igre. Da ne bi morali da pišemo brdo koda, iskoristićemo SDL biblioteku za rad sa prozorima i OpenGL(ES) za crtanje.
Počećemo pisanjem koda koji će biti jednak za sve platforme, i taj kod će biti podeljen na 4 dela:
Inicijalizacija
Obrada poruke o promeni veličine prozora
Animacija
Crtanje
Inicijalizacija će biti prilično jednostavna. Pošto će kod za kreiranje prozora pripremiti i OpenGL, nama ovde ostaje samo postavljanje osnovnih parametara za crtanje:
- procedure Init;
- begin
- glDisable(GL_LIGHTING);
- glDisable(GL_CULL_FACE);
- end;
Obrada poruke o promeni veličine prozora je takođe jednostavna. Jedini trik je da za OpenGL (desktop plathorme) zovemo funkciju glFrustum, a za OpenGL ES glFrustumf:
- procedure Resize(Width, Height: Integer);
- var
- X, Y: Single;
- begin
- glViewport(0, 0, Width, Height);
-
- glMatrixMode(GL_PROJECTION);
- glLoadIdentity;
- Y := Sin(45 * pi / 360) / Cos(45 * pi / 360);
- X := Y * (Width / Height);
- {$IFDEF CPUARM}
- glFrustumf(-X, X, -Y, Y, 1, 100);
- {$ELSE}
- glFrustum(-X, X, -Y, Y, 1, 100);
- {$ENDIF}
-
- glMatrixMode(GL_MODELVIEW);
- glLoadIdentity;
- glTranslatef(0, 0, -5);
- end;
Procedura za animaciju će kao parametar dobiti vreme koje je prošlo od zadnjeg crtanja i samo će rotirati celu scenu:
- procedure Update(DeltaTime: Integer);
- begin
- glRotatef(DeltaTime / 10, 0, 1, 0);
- end;
Crtanje je u našem primeru takođe jednostavno... samo jedan trouglić:
- var
- Vertices: array[0..20] of Single = (
- 0, 1, 0, 1, 0, 0, 1,
- -1, -1, 0, 0, 1, 0, 1,
- 1, -1, 0, 0, 0, 1, 1
- );
-
- procedure Render;
- begin
- glClear(GL_COLOR_BUFFER_BIT);
- glEnableClientState(GL_VERTEX_ARRAY);
- glVertexPointer(3, GL_FLOAT, 28, @Vertices[0]);
- glEnableClientState(GL_COLOR_ARRAY);
- glColorPointer(4, GL_FLOAT, 28, @Vertices[3]);
- glDrawArrays(GL_TRIANGLES, 0, 3);
- glDisableClientState(GL_VERTEX_ARRAY);
- glDisableClientState(GL_COLOR_ARRAY);
- end;
Sada se bacamo na kod koji će na desktop platformama kreirati prozor i vrteti glavnu petlju. Kao što rekoh, koristićemo SDL da ne bi morali da pišemo poseban kod za svaku platformu:
- program GLDemo;
-
- {$R *.res}
-
- uses
- sysutils, sdl, GLApp;
-
- var
- Window: PSDL_Surface;
- Event: TSDL_Event;
- Running: Boolean;
- CurrentTime, NewTime: Cardinal;
-
- begin
- try
- if SDL_Init(SDL_INIT_VIDEO) <> 0 then
- raise Exception.Create('Failed to initialize SDL: ' + SDL_GetError);
-
- SDL_putenv('SDL_VIDEO_CENTERED=1');
- SDL_ShowCursor(SDL_DISABLE);
- SDL_WM_SetCaption('GLDemo', 'GLDemo');
- Window := SDL_SetVideoMode(800, 600, 32, SDL_DOUBLEBUF or SDL_OPENGL or SDL_RESIZABLE);
- if Window = nil then
- raise Exception.Create('Failed to set video mode: ' + SDL_GetError);
-
- Init;
- Resize(800, 600);
-
- Running := True;
- CurrentTime := SDL_GetTicks;
- while Running do
- begin
- while SDL_PollEvent(@Event) <> 0 do
- if Event.type_ = SDL_QUITEV then
- Running := False
- else if Event.type_ = SDL_VIDEORESIZE then
- Resize(Event.resize.w, Event.resize.h);
-
- NewTime := SDL_GetTicks;
- Update(NewTime - CurrentTime);
- CurrentTime := NewTime;
- Render;
-
- SDL_GL_SwapBuffers;
- end;
- finally
- SDL_Quit;
- end;
- end.
Ova aplikacija će pokrenuti SDL i kreirati prozor. Odmah posle kreiranja prozora, poziva Init funkciju koju smo ranije definisali, a zatim i Resize funkciju kako bi program pravilno postavio parametre za crtanje. Zatim ulazi u glavnu petlju u kojoj poziva Update i Render funkcije koje se brinu o crtanju. U slučaju da se veličina prozora ponovo promeni, zove se funkcija Resize i prosleđuju se nove dimenzije.
Ništa specijalno, ali zahvaljujući tome što smo koristili SDL i OpenGL, ova aplikacija već sada može da se iskompalira i pokrene na Windowsu, Linuxu i Macu.
Za sada je sve bilo prilično standardno, ali se za Android moramo malo više pomučiti. Problem je u tome što deo aplikacije za kreiranje prozora moramo napisati u Javi, što za sobom povlači i to da naš kod za logiku, kojeg smo ranije napisali, moramo kompajlirati kao biblioteku, a ne kao izvršni program. Da stvar bude još gora, funckije u biblioteci moraju biti napisane na tačno definisan način kako bi Java znala da ih pozove.
Da krenemo s Java delom... on se piše na manje-više isti način kao kad bi želeli da koristite OpenGL u Javi:
- package srki.android;
-
- import javax.microedition.khronos.egl.EGLConfig;
- import javax.microedition.khronos.opengles.GL10;
-
- import android.app.Activity;
- import android.content.Context;
- import android.opengl.GLSurfaceView;
- import android.opengl.GLSurfaceView.Renderer;
- import android.os.Bundle;
- import android.os.SystemClock;
- import android.view.Window;
- import android.view.WindowManager;
-
- public class GLDemo extends Activity {
-
- private SceneView glView = null;
-
- private native void NativeInit();
- private native void NativeResize(int width, int height);
- private native void NativeUpdate(int deltaTime);
- private native void NativeRender();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- this.requestWindowFeature(Window.FEATURE_NO_TITLE);
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
- glView = new SceneView(this);
- setContentView(glView);
- System.loadLibrary("gldemo");
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- glView.onPause();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- glView.onResume();
- }
-
- public class SceneView extends GLSurfaceView {
-
- public SceneView(Context context) {
- super(context);
- setRenderer(new SceneRenderer());
- }
- }
-
- public class SceneRenderer implements Renderer {
-
- private long currentTime, newTime;
-
-
- @Override
- public void onDrawFrame(GL10 gl) {
- newTime = SystemClock.elapsedRealtime();
- NativeUpdate((int)(newTime - currentTime));
- currentTime = newTime;
- NativeRender();
- }
-
- @Override
- public void onSurfaceChanged(GL10 gl, int width, int height) {
- NativeResize(width, height);
- }
-
- @Override
- public void onSurfaceCreated(GL10 gl, EGLConfig config) {
- currentTime = SystemClock.elapsedRealtime();
- NativeInit();
- }
-
- }
- }
Na standardan način kreiramo Activity, GLSurfaceView i Renderer... isto kao da bi posle toga krenuli sa standardnim crtanjem u Javi... razlika je u tome, da osim standardnih stvari pozivamo i System.loadLibrary("gldemo"). Ta linija će učitati biblioteku libgldemo.so i iz nje pročitati native funkcije koje smo definisali na početku koda. Imena tih funkcija su mogle biti iste kao i u kodu za logiku (bez Native prefiksa), ali dodavanjem prefiksa olakšavamo kasnije popravljanje koda, jer na osnovu imena znamo da li je funkcija definisana u Javi ili u nekoj biblioteci. Kada su funkcije pročitane iz biblioteke, možemo ih koristiti kao da smo ih definisali u kodu u Javi. Ovaj mali programčić poziva NativeInit čim je OpenGL ES spreman za rad, NativeResize prilikom promene veličine prozora, i NativeUpdate i NativeRender kad god je potrebno da se scena nacrta na ekran.
Stižemo i do zadnjeg dela... biblioteka koju će Java aplikacija koristiti mora biti napisana u skladu sa JNI pravilima: [Link mogu videti samo ulogovani korisnici]
- library GLDemoAndroid;
-
- {$linklib c}
-
- uses
- jni, GLApp;
-
- function JNI_OnLoad(VM: PJavaVM; Reserved: Pointer): jint; cdecl;
- begin
- Result := JNI_VERSION_1_4;
- end;
-
- function JNI_OnUnload(VM: PJavaVM; Reserved: Pointer): jint; cdecl;
- begin
- Result := 0;
- end;
-
- procedure Java_srki_android_GLDemo_NativeInit(JNIEnv: PJNIEnv; Obj: jobject); cdecl;
- begin
- Init;
- end;
-
- procedure Java_srki_android_GLDemo_NativeResize(JNIEnv: PJNIEnv; Obj: jobject; Width, Height: jint); cdecl;
- begin
- Resize(Width, Height);
- end;
-
- procedure Java_srki_android_GLDemo_NativeUpdate(JNIEnv: PJNIEnv; Obj: jobject; DeltaTime: jint); cdecl;
- begin
- Update(DeltaTime);
- end;
-
- procedure Java_srki_android_GLDemo_NativeRender(JNIEnv: PJNIEnv; Obj: jobject); cdecl;
- begin
- Render;
- end;
-
- exports
- JNI_OnLoad,
- JNI_OnUnload,
- Java_srki_android_GLDemo_NativeInit,
- Java_srki_android_GLDemo_NativeResize,
- Java_srki_android_GLDemo_NativeUpdate,
- Java_srki_android_GLDemo_NativeRender;
-
- end.
Kratko objašnjenje za one koje mrzi da pročitaju dokumentaciju... biblioteka mora imati najmanje 2 funkcije i to JNI_OnLoad i JNI_OnUnload. JNI_OnLoad vraća verziju JNI sa kojom biblioteka zna da radi. Preko parametara te funckije je moguće doći i do funkcija koje su definisane u Java kodu (koga to zanima, neka pročita dokumentaciju). JNI_OnUnload funkcija u suštini ne služi ničemu... u dokumentaciji piše da se ona poziva kada JNI završi sve što ima s bibliotekom, ali pošto java koristi GC, ne zna se kad će tačno (i da li će) biti pozvana.
Funkcije koje nudimo javi moraju imati sledeći oblik Java_ImePaketa_ImeKlase_ImeFunkcije. ImePaketa je ime Java paketa u kojem je definisana klasa u kojoj su upisane definicije native funkcija, i umesto tački se koriste donje crte. Pošto je naša Java aplikacija u paketu srki.android, a klasa se zove GLDemo sve funkcije počinju sa Java_srki_android_GLDemo_. Prva dva parametra su obavezna i služe za integraciju sa JNI, a posle njih slede parametri koje želimo da prosleđujemo našim funkcijama. Kao što se vidi iz koda, JNI funkcije jednostavno zovu funkcije koje smo prethodno definisali, i koje koristimo i u desktop verziji aplikacije.
Par bitnih stvari na koje treba obratiti pažnju prilikom kompajliranja ove biblioteke: treba izabrati Linux u polju Target OS, i arm u polju Target CPU family; obavezno postaviti put do Android NDK biblioteka; u slučaju da Java aplikacija dobije grešku prilikom učitavanja biblioteke, staviti optimization level na 1 ili 0.
Kada je biblioteka iskompajlirana, potrebno ju je postaviti na pravo mesto kako bi Java kod mogao da ga nađe. Biblioteka se mora nalaziti u direktorijumu: ImeProjekta/libs/armeabi. U slučaju da niste sigurni, pogledajte Android projekat na kraju ovog teksta i sve će vam biti jasno.
I, to je to... videli smo da kreiranje prozora mora da se napiše za svaku platformu posebno, ili da se koristi neka biblioteka koja može da nam olakša posao (SDL, SFML, Allegro, itd...), ali samu logiku programa/igre pišemo samo jednom.
Evo i par sličica sa svih platformi:
Android

Linux

Mac OS X

Windows

Kod možete preuzeti ovde: [Link mogu videti samo ulogovani korisnici]
Eclipse projekat za Android možete preuzeti ovde: [Link mogu videti samo ulogovani korisnici]
|