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: http://java.sun.com/docs/books/jni/html/jniTOC.html
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: https://www.mycity.rs/must-login.png
Eclipse projekat za Android možete preuzeti ovde: https://www.mycity.rs/must-login.png
|