יום שני, מרץ 21

AndEngine חלק 1.

מחר, ב-22 למרץ 2001 בשעה 18:30 יתקיים מפגש מפתחי אנדרואיד. בין היתר, אורן בן-גיגי ידבר על  AndEngine - פלטפורמת פיתוח למשחקים. אני בטוח שיהיה מעניין, וכמתאבן  להרצאה , החלטתי להוסיף פוסט בנושא. 

AndEngine מבוססת על ה-OpenGL-ES ומוגדרת ע"י מפתחה  Nicolas Gramlich כ: Free Android 2D OpenGL Game Engine.
הנה קישור לאתר AndEngine עם Tutorial בנושא: 
ניתן להוריד מהאתר הנ"ל חבילת דוגמאות מצוינות (ראו בקישור הנ"ל), ואכן זהו כלי שמאפשר פיתוח גרפיקה דו מימדית במהירות.
ישנן אגב פלטפורמות משחקים נוספות לאנדרואיד כמו Libgdx, Android-2D-Engine, Rokon ועוד.   מהתבוננות בחבילה נראה שהמימוש שלה מתבסס גם  על NDK   כלומר c/c++. היא משתמשת ב-OpenGl  אבל כנראה לא בספריות המיוחדות שאנדרואיד מספקת כדי להתאים את  ה-Dalvik VM ל-OpenGl (ראה את הפוסטים  בנושא OpenGl בבלוג זה. ) אלא בספריות C מקוריות.
מספר שאלות עשויות לעלות בהקשר למערכת - למשל, האם יש בעית ביצועים, האם המימוש ב-C יצור בעיית תאימות עבור מעבדים עתידיים (אינטל צפויים להכנס למשחק בקרוב?)
ההכרות שלי עם החבילה הזו מאד שטחית. כפי שציינתי למעלה, הדוגמאות שמסופקות מצוינות. אבל בתור מתחיל, חוסר התעוד מפריע.  מעבר לחומר שנמצא באתר הרשמי - וכפי שראיתם בודאי, הוא די מצומצם, לא מצאתי מקורות נוספים לקבלת מידע על ה-classes.  מה שנותר (לי) לעשות הוא לשחק עם הפרמטרים ועם ה-classes ולהבין מה קורה שם. 
אם לשתף אתכם בנסיוני, אז התחלתי להריץ את הדוגמאות, לבנות פרויקטים שלי המבוססים עליהן, לשחק עם הפרמטרים, ולראות מה יקרה :-).  ניקח למשל את ה-Moving Ball Example. הכדור אמור לרוץ מצד לצד. אצלי הוא פשוט נפל מלמעלה עד לתחתית ונעצר למרות שלא נגעתי בקוד המקורי. לפי הבדיקה שלי המהירות שלו לא היתה 0.  זמן רב נאלצתי לשרוף על זה. זה ממחיש את הבעיתיות שבהעדר תעוד. בסוף הסתבר שתיקון הקוד במקום  בו הוגדרה לכדור המהירות התחילית מתקן את הבעיה.  הנה הקוד המתוקן (מתנצל שאני לא צובע אותו הפעם - בינתיים). 
התיקון הנדרש כולל מחיקת 3  שורות ב- onLoadScene והוספת שורה ב- Ball. ראה שורות באדום למטה.

import org.anddev.andengine.engine.Engine;
import org.anddev.andengine.engine.camera.Camera;
import org.anddev.andengine.engine.handler.physics.PhysicsHandler;
import org.anddev.andengine.engine.options.EngineOptions;
import org.anddev.andengine.engine.options.EngineOptions.ScreenOrientation;
import org.anddev.andengine.engine.options.resolutionpolicy.RatioResolutionPolicy;
import org.anddev.andengine.entity.scene.Scene;
import org.anddev.andengine.entity.scene.background.ColorBackground;
import org.anddev.andengine.entity.sprite.AnimatedSprite;
import org.anddev.andengine.entity.util.FPSLogger;
import org.anddev.andengine.opengl.texture.Texture;
import org.anddev.andengine.opengl.texture.TextureOptions;
import org.anddev.andengine.opengl.texture.region.TextureRegionFactory;
import org.anddev.andengine.opengl.texture.region.TiledTextureRegion;

import android.util.Log;

/**
 * @author Nicolas Gramlich
 * @since 11:54:51 - 03.04.2010
 */
public class MovingBallExample extends BaseExample {
// ===========================================================
// Constants
// ===========================================================

private static final int CAMERA_WIDTH = 720;
private static final int CAMERA_HEIGHT = 480;

private static final float DEMO_VELOCITY = 100.0f;

// ===========================================================
// Fields
// ===========================================================

private Camera mCamera;

private Texture mTexture;
private TiledTextureRegion mFaceTextureRegion;

// ===========================================================
// Constructors
// ===========================================================

// ===========================================================
// Getter & Setter
// ===========================================================

// ===========================================================
// Methods for/from SuperClass/Interfaces
// ===========================================================

@Override
public Engine onLoadEngine() {
this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE, new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), this.mCamera));
}

@Override
public void onLoadResources() {
this.mTexture = new Texture(64, 32, TextureOptions.BILINEAR_PREMULTIPLYALPHA);
this.mFaceTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mTexture, this, "gfx/face_circle_tiled.png", 0, 0, 2, 1);

this.mEngine.getTextureManager().loadTexture(this.mTexture);
}

@Override
public Scene onLoadScene() {
this.mEngine.registerUpdateHandler(new FPSLogger());

final Scene scene = new Scene(1);
scene.setBackground(new ColorBackground(0.09804f, 0.6274f, 0.8784f));

final int centerX = (CAMERA_WIDTH - this.mFaceTextureRegion.getWidth()) / 2;
final int centerY = (CAMERA_HEIGHT - this.mFaceTextureRegion.getHeight()) / 2;
final Ball ball = new Ball(centerX, centerY, this.mFaceTextureRegion);
//final PhysicsHandler physicsHandler = new PhysicsHandler(ball);
// ball.registerUpdateHandler(physicsHandler);
//physicsHandler.setVelocity(DEMO_VELOCITY, DEMO_VELOCITY);

scene.getLastChild().attachChild(ball);

return scene;
}

@Override
public void onLoadComplete() {

}

// ===========================================================
// Methods
// ===========================================================

// ===========================================================
// Inner and Anonymous Classes
// ===========================================================

private static class Ball extends AnimatedSprite {
private final PhysicsHandler mPhysicsHandler;

public Ball(final float pX, final float pY, final TiledTextureRegion pTextureRegion) {
super(pX, pY, pTextureRegion);
this.mPhysicsHandler = new PhysicsHandler(this);
this.registerUpdateHandler(this.mPhysicsHandler);
            mPhysicsHandler.setVelocity(DEMO_VELOCITY, DEMO_VELOCITY);
}

@Override
protected void onManagedUpdate(final float pSecondsElapsed) {
Log.d("mx+getWidth()="+mX+getWidth(),"mY+getHeight() = "+mY+getHeight());
if(this.mX < 0) {
this.mPhysicsHandler.setVelocityX(DEMO_VELOCITY);
} else if(this.mX + this.getWidth() > CAMERA_WIDTH) {
this.mPhysicsHandler.setVelocityX(-DEMO_VELOCITY);
}

if(this.mY < 0) {
this.mPhysicsHandler.setVelocityY(DEMO_VELOCITY);
} else if(this.mY + this.getHeight() >= CAMERA_HEIGHT) {
this.mPhysicsHandler.setVelocityY(-DEMO_VELOCITY);
}
// else{
// this.mPhysicsHandler.setVelocityY(-DEMO_VELOCITY);
// }
Log.d(" Velocity x= "+this.mPhysicsHandler.getVelocityX(), "Velocity Y = "+this.mPhysicsHandler.getVelocityY());

Log.d(" Velocity x= "+this.mPhysicsHandler.getVelocityX(), "Velocity Y = "+this.mPhysicsHandler.getVelocityY());
super.onManagedUpdate(pSecondsElapsed);
}
}
}
דגשים ביצירת פרויקט 
לגבי תהליך בניית הפרויקט - הוא מוסבר בקישור שנתתי ובוידאו המצורף.  פתיחת הפרויקט נעשית כרגיל + צריך להוסיף הספריות libs  ו- assets ולהעתיד ספריות ל-lib.
ה-BaseGameActivity הוא ה-class  הראשי של החבילה, ואותו יש לרשת.
כדי לפתוח class  חדש, הכניסו את השלד הבא:

public class TestGameActivity extends BaseGameActivity{
}
כעת, כרגיל, ללחוץ  ctrl-shift-o לקבלת רשימת הספריות ליבוא. כרגע תקבלו:
import org.anddev.andengine.ui.activity.BaseGameActivity;

ה-BaseGameActivity מחייב מימוש של 4 מתודות. כד לקבל את השלד שלהם עשו:
  1. קליק ימני על BaseGameActivity.
  2. source->override/implement methods->BaseGameActivity

נקבל את השלדים של 4 המתודות. כעת ה-class יראה כך:

import org.anddev.andengine.engine.Engine;
import org.anddev.andengine.entity.scene.Scene;
import org.anddev.andengine.ui.activity.BaseGameActivity;

public class TestGameActivity extends BaseGameActivity{

@Override
public Engine onLoadEngine() {
// TODO Auto-generated method stub
return null;
}

@Override
public void onLoadResources() {
// TODO Auto-generated method stub
}

@Override
public Scene onLoadScene() {
// TODO Auto-generated method stub
return null;
}

@Override
public void onLoadComplete() {
// TODO Auto-generated method stub
}

}

סדר הפעולה של 4 ה-callbacks האלה הוא:

onLoadEngine-> onLoadResources-> onLoadScene-> onLoadCom

נגדיר את משתני ה-class להם נזדקק:

private static final int CAMERA_WIDTH = 720;
private static final int CAMERA_HEIGHT = 480;
private Camera mCamera;

בנקודה זו רציתי להכניס דוגמא שלי לשימוש ב-AndEngine. תוך כדי כתיבה שיניתי דעתי והגעתי למסקנה שעדיף להתחיל עם דוגמא פשוטה מתוך הדוגמאות שמספק  AndEngine - שוב, ראו קישור למעלה.
שם הדוגמא: MovingBallExample. זהו חלק שנמצא בתוך חבילת הדוגמאות.
הדוגמאות האלה מצוינות, שלא להזכיר את העבודה שנחסכת לי .
היות שכך, לפני שניגש לקוד, הנה צילום תמונת המסך של הכדור שנעה על המסך:




נעבור על המתודות, תחילה 4 ה-callbacks שחייבים לממש. נעבור לפי סדר הביצוע, תחילה onLoadEngine.

  1. @Override
  2. public Engine onLoadEngine() {
  3. this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
  4. return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE, new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), this.mCamera));
  5. }


הנה כמה גדרות ל-classes בהן נשתמש:

Camera - מגדירה את מלבן התצוגה על המסך.
Engine - אחראי על הפעלת מהלך  המשחק - הפעלת התהליכים המחזורים ועידכון ה-Scene.
Scene - זהו ה-class שמכיל את כל האובייקטים שיוצגו על המסך.
RatioResolutionPolicy - זהו חלק מה- EngineOptions. קובע את יחסי התצוגה בצגים השונים, עם מגבלה לגודל המקסימלי. אם במקום זה נעדיף שימוש במסך מלא בכל מכשיר, יש להשתמש ב: ()FillResolutionPolicy.


נעבור למתודה הבאה, onLoadResources:



  1. @Override
  2. public void onLoadResources() {
  3. this.mTexture = new Texture(64, 32, TextureOptions.BILINEAR_PREMULTIPLYALPHA);
  4. this.mFaceTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mTexture, this, "gfx/face_circle_tiled.png", 0, 0, 2, 1);

  5. this.mEngine.getTextureManager().loadTexture(this.mTexture);
  6. }


נמשיך בהגדרות:
 Texture  - תמונה. היא מאוכסנת בזיכרון של המאיץ הגראפי.  מידות האורך והרוחב חייבים להיות חזקות של 2 (2,4,8,16...). אחרת - צפו להתרסקויות. חסכו לעצמכם את הזמן שאחרים ביזבזו כבר. אם התמונה אינה בגודל,  הפיתרון הסביר הוא לעגל את הערכים כלפי מעלה.

שורה מס 3:


Texture (int width, int height, TextureOptions  options) 


האופציה בשורה 3 היא BILINEAR_PREMULTIPLYALPHA. מה זה אומר? 
PREMULTIPLYALPHA - ערכי ה-RGB של הפיקסל מוכפלים מראש במקדם הבהירות אלפא, כך למשל RGB = 1,1,0 עם אלפא = 0.5 יהיה RGBA = 0.5,0.5,0,0.5.
אצלנו מדובר ב-BILINEAR_PREMULTIPLYALPHA כאשר BILINEAR מתייחס לfilter שמשמש להכפלת האלפא וערך הפיקסל. ה-filter  מחליק את התמונה ע"י התחשבות בפיקסלים השכנים. 



שורה 4 מגדירה את ה-texture region  בו נמצא ה-sprite  - התמונה כולה גדולה יותר, אבל צריך לשלוף ממנה את החלק הדרוש.

this.mFaceTextureRegion = TextureRegionFactory.createTiledFromAsset(this.mTexture, this, "gfx/face_circle_tiled.png", 0, 0, 2, 1);

הפרמטרים של המתודה הם:
  1. ה-Texture שמכיל את התמונה.
  2. ה-context של האפליקציה.
  3. התמונה שתטען. היא ממוקמת ב- assets/gfx.
  4. מיקום ה-sprite בתמונה. שני הפרמטרים הראשונים הם x,y  של הפינה השמאלית העליונה, ושני האחרונים הם x,y של הפינה הימנית התחתונה.
לסיום,  שורה 6, טעינת כל זה ל- Game Engine:

this.mEngine.getTextureManager().loadTexture(this.mTexture);

סיימנו את המתודה הזו. התמונה שלנו - הכדור - טעון ב- Game Engine. 
נעבור למתודה הבאה, onLoadScene.


  1. @Override
  2. public Scene onLoadScene() {
  3. this.mEngine.registerUpdateHandler(new FPSLogger());

  4. final Scene scene = new Scene(1);
  5. scene.setBackground(new ColorBackground(0.09804f, 0.6274f, 0.8784f));

  6. final int centerX = (CAMERA_WIDTH - this.mFaceTextureRegion.getWidth()) / 2;
  7. final int centerY = (CAMERA_HEIGHT - this.mFaceTextureRegion.getHeight()) / 2;
  8. final Ball ball = new Ball(centerX, centerY, this.mFaceTextureRegion);
  9. //final PhysicsHandler physicsHandler = new PhysicsHandler(ball);
  10. // ball.registerUpdateHandler(physicsHandler);
  11. //physicsHandler.setVelocity(DEMO_VELOCITY, DEMO_VELOCITY);

  12. scene.getLastChild().attachChild(ball);

  13. return scene;
  14. }


כאן נגדיר את ה-sprite ונציג אותו ע"י טעינתו ל-scene.
שורה 3 רגיסטרציה של updatehandler ל-engine עם Frame Per Second logger.
שורה 5: יצירה ואיתחול של scene - ראה הגדרה למעלה. 
שורות 7,8 נותנות את קואורדינטות המיקום בו נמקם את הפרצוף.
שורה 9 - יצירת אובייקט מסוג Ball. ה-constructor שלו מקבל את קואורדינטות המיקום ומשטח ה-texture. נגיע ל-Ball בהמשך.
שורה 13: הוספת ה-Ball  ל-scene. זהו. יש לנו תמונה.

ה-callback הרביעי, onLoadComplete, לא ממומש. ברור משמו מתי הוא מופעל.

הנה ה-Ball:


private static class Ball extends AnimatedSprite {
private final PhysicsHandler mPhysicsHandler;

public Ball(final float pX, final float pY, final TiledTextureRegion pTextureRegion) {
super(pX, pY, pTextureRegion);
this.mPhysicsHandler = new PhysicsHandler(this);
this.registerUpdateHandler(this.mPhysicsHandler);
            this.mPhysicsHandler.setVelocity(DEMO_VELOCITY, DEMO_VELOCITY);
}

הוא די ברור. מקוצר הזמן לא אוסיף הסברים. אולי אעשה זאת בהמשך השבוע.

גם onManagedUpdate  נראית כמסבירה את עצמה.

להתראות מחר במפגש!






3 תגובות:

  1. תודה רבה על ההשקעה , נראה ממש מפורט ומובן אחרי ההסבר שלך .

    השבמחק
  2. לאוניד, תודה על המשוב. הפוסט הזה הוא ממש בגדר מבוא ראשוני. מקווה להוסיף פוסטים נווספים.

    השבמחק