Browsed by
קטגוריה: libGDX

libGDX Scene2d tutorial – Part 2 – Splash Screen

libGDX Scene2d tutorial – Part 2 – Splash Screen

(חלק 1 נמצא כאן)

עברנו את השלב הראשון – טענו את קבצי התמונות/קול לזיכרון. לפני שאנחנו עוברים למשחק עצמו, אנחנו רוצים להציג את הלוגו של חברת המשחקים שלנו.

משהו כזה:

Splash Screen

כדי לעשות זאת, נשתמש (לראשונה) בספריית Scene2d.

Read More Read More

libGDX Scene2d tutorial – Part 1 – Loading Screen

libGDX Scene2d tutorial – Part 1 – Loading Screen

בעבר כתבתי על איך ליצור פרוייקט libGDX, ואיך לכתוב משחק פשוט. היום נתחיל בסדרת מדריכים לכתיבת משחק ב- libGDX עם שימוש ב- Scene2d – ספרייה שמקלה על ניהול ה- UI.

התחלה

ניצור פרוייקט חדש. אין צורך ב- Box2d או ספריות נוספות, scene2d מגיעה באופן מובנה בתוך libGDX.

המשחק שלנו יכלול 5 מסכים:

  • Loading – מסך טעינה, בו נטען לזיכרון את כל ה- assets הדרושים, כמו תמונות, קבצי קול וכו׳.
  • Splash – מסך ספלאש, בו נציג את הלוגו שלנו.
  • Menu – מסך תפריט ראשי, בו נציג את ממשק ניהול המשחק (כפתור להתחלת משחק, שיתוף וכו׳).
  • Play – מסך משחק, העיקר.
  • Game Over – מסך סיום משחק בו נציג את התוצאה.


Read More Read More

libGDX Fonts

libGDX Fonts

איך מכניסים טקסט לתוך משחק שנכתב ב- libGDX?

אז ככה. מכיוון שכל דבר אותו אנו יוצרים בספרייה הזו יתורגם בסופו של דבר ל- OpenGL, גם הטקסט הוא כזה. כלומר ההתייחסות בסופו של דבר היא בתור bitmap – לצייר את צורת האותיות על המסך.

נשתמש בתוכנה בשם Hiero (אפשר להוריד מכאן):

screen-shot-2016-11-14-at-13-30-03

נבחר את הפונט הרצוי, Rendering מסוג Java, ואז בתפריט File נבחר 'Save BMFont files'.

התוצר יהיה שני קבצים: png, ו- fnt.

קובץ התמונה יראה משהו כמו זה:

text

הקובץ השני מכיל את המיקומים של האותיות ברחבי התמונה.

דגשים חשובים:

  • תחשבו טוב טוב על הגודל אותו אתם הולכים לתת לפונט – לא תוכלו אח״כ להשתמש בו ביותר גדול, כי זה יראה מפוקסל – רק ביותר קטן. מצד שני, לא כדאי להגזים עם הגודל – זה סתם יתפוס מקום מיותר בזיכרון.
  • אולי לא קשור ישירות לנושא, אבל עדיין חשוב: זה שיש לכם פונט מותקן במחשב לא בהכרח אומר שמותר לכם לעשות בו שימוש מסחרי בתוך אפליקציה. עדיף למצוא פונט שבוודאות הרישיון שלו מתיר שימוש חופשי (כאן יש הרבה כאלו).

אז אחרי שיצרנו את שני הקבצים הנ״ל, נוסיף אותם לתיקיית ה- assets לתוך תיקייה חדשה בשם fonts.

כדי להשתמש בפונט נעזר במחלקה BitmapFont בצורה הזו:

BitmapFont font = new BitmapFont(Gdx.files.internal("fonts/text.fnt"));
...
 
// on rendering
batcher.begin();
font.draw(batcher, "Hello World", x, y);
batcher.end();
 
// on destroy
font.dispose();

זה טוב ונחמד. אבל מה אם (משום מה) יש לנו אפליקציה בה אנחנו נדרשים לעשרות פונטים, או אפילו צריכים ליצור פונט בזמן אמת, מקובץ ttf?

בשביל זה יש תוסף בשם FreeType. ניצור פרוייקט חדש עם freetype מסומן:

freetype_libgdx

כשנרצה לייצר BitmapFont במהלך המשחק, נעשה כך:

public class FontsExample extends ApplicationAdapter {
	SpriteBatch batch;
	BitmapFont font;
	@Override
	public void create () {
		batch = new SpriteBatch();
		FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal("fonts/Xenotron.ttf"));
		FreeTypeFontGenerator.FreeTypeFontParameter parameter = new FreeTypeFontGenerator.FreeTypeFontParameter();
		parameter.size = 20;
		font = generator.generateFont(parameter);
		font.setColor(Color.WHITE);
		generator.dispose(); // don't forget to dispose to avoid memory leaks!
	}
 
	@Override
	public void render () {
		Gdx.gl.glClearColor(1, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
		batch.begin();
 
		font.draw(batch, "Hello", 100, 100);
		batch.end();
	}
 
	@Override
	public void dispose () {
		batch.dispose();
		font.dispose();
	}
}

 

libGDX physics with Box2D

libGDX physics with Box2D

אחד הנושאים המורכבים ביותר בפיתוח משחקים הוא השימוש במנוע הפיזיקלי. בפוסט הקרוב אנסה להסביר על איך להשתמש בעולם פיזיקלי של Box2D, בסביבת libGDX.

תאוריה

ניצור עולם – מרחב בו אנו מגדירים מהם הכוחות הפועלים. מה יהיה כוח המשיכה בציר ה- Y (אפשר 9.81, אבל לא חייבים), מה כוח המשיכה בציר ה- X (אם רוצים).
לתוך העולם הזה ניצור "גופים". לכל גוף יש הגדרות כמו לגוף בעולם האמיתי: צורה, מסה, מהירות וכו'.

ישנם שלושה סוגי גופים:

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

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

ועכשיו למעשה.

קוד

ניצור פרוייקט חדש בשם Box2dExample, ונבחר ב- extensions את הספרייה Box2D.

נלך למחלקה ב- Core שנקראת Box2dExample, ונוסיף את המשתנים הבאים:

private World world;
	private Box2DDebugRenderer b2dr;
	private OrthographicCamera b2dCam;
	private int SCREEN_WIDTH = 480;
	private int SCREEN_HEIGHT = 800;

המחלקה שאיתה יוצרים עולם ב- Box2d נקראת (באופן מפתיע) World. כדי לדבג אותה נשתמש באובייקט מסוג Box2DDebugRenderer, שיודע לרנדר את כל הגופים שקיימים כרגע בעולם.

נוסיף לפונקציית create את הקוד הבא:

public void create () {
		world = new World(new Vector2(0, 9.81f), false);
		b2dCam = new OrthographicCamera();
		b2dCam.setToOrtho(true, SCREEN_WIDTH,
				SCREEN_HEIGHT);
		b2dr = new Box2DDebugRenderer();
 
		createBall(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
		createGround(new Rectangle(SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.75f, 100, 20));
	}

כדי ליצור עולם דרושים בסך הכל שני פרמטרים: וקטור של כוח המשיכה הפועל בעולם (במקרה שלנו זה כמו בעולם האמיתי – 0 בציר ה- x ו- 9.81 מטר לשניה בציר ה- y). ומשתנה בוליאני שקובע האם לפעול גם על גופים שאינם פעילים (לשיפור ביצועים עדיף false).

זה היה קל. עכשיו נעבור לגופים.

נתחיל בגוף סטטי:

private void createGround(Rectangle rect){
		BodyDef bdef = new BodyDef();
 
		bdef.position.set(rect.getX(), rect.getY());
		bdef.type = BodyDef.BodyType.StaticBody;
		Body body = world.createBody(bdef);
 
		PolygonShape shape = new PolygonShape();
		shape.setAsBox(rect.getWidth(), rect.getHeight());
		FixtureDef fdef = new FixtureDef();
		fdef.friction = 0.5f;
		fdef.shape = shape;
 
		body.createFixture(fdef).setUserData("ground");
 
		body.setUserData(this);
 
 
		shape.dispose();
	}

כדי ליצור Body, צריך קודם כל ליצור BodyDef שם מגדירים את סוג הגוף ומיקומו.

אז מגדירים את ה- Shape בו רוצים להשתמש, והגודל שלו (חשוב לזכור לעשות dispose אחרי השימוש בו!).

ואז מגדירים FixtureDef עם כל התכונות שנרצה לגוף. וזהו.

באותה הדרך נגדיר גם גוף דינמי:

private void createBall(int x, int y){
		BodyDef bdef = new BodyDef();
 
		bdef.position.set(x, y);
		bdef.type = BodyDef.BodyType.DynamicBody;
		Body ball = world.createBody(bdef);
 
		CircleShape shape = new CircleShape();
		shape.setRadius(20f);
		FixtureDef fdef = new FixtureDef();
		fdef.restitution = 0.7f;
		fdef.density = 2f;
		fdef.friction = 0.25f;
		fdef.shape = shape;
 
		ball.createFixture(fdef).setUserData("ball");
		ball.setUserData(this);
		shape.dispose();
	}

התכונה "restitution" (פיצוי) קובעת בזמן התנגשות כמה מתוך הכוח יוחזר. כלומר אם הערך יהיה ״1״ הכדור יפול ויעלה לנצח.

נוסיף לפונקציית render את הקוד הבא:

public void render () {
		Gdx.gl.glClearColor(0, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
 
		world.step(Gdx.graphics.getDeltaTime(), 6, 2);
 
		b2dr.render(world, b2dCam.combined);
	}

כדי שה״עולם״ יתעדכן צריך בכל פעם לקרוא ל- step.

אם נריץ עכשיו, זה מה שנקבל:

box2d

האובייקט Box2DDebugRenderer מצייר בצבע שונה כל סוג של Body. ירוק זה גופים סטטיים, וכו׳.

נראה טוב, לא? אבל… זה זז קצת לאט. אפילו מאד לאט.

צריך לקחת בחשבון את שיטת המדידה שבה משתמשים ב- Box2d. כלומר כשקבענו שהכוחות הפועלים הם 9.81 מטר לשניה – לא הגדרנו מה נקרא מטר. ה- default זה שכל פיקסל הוא מטר אחד… כלומר מה שאנחנו רואים כאן זה בעצם כדור בקוטר 40 מטר נופל על משטח בגובה 20 מטר. זה כבר יותר מובן.

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

נוסיף משתנה בשם PPM – כלומר Pixels Per Meter:

private float PPM = 100;

ובכל מקום בו יש התייחסות למיקום/גודל על גבי המסך, נחלק ב- PPM, וגם את הגדרת גודל המצלמה בה אנחנו משתמשים b2dCam נחלק ב- PPM.

אחרי השינוי זה יראה ככה:

box2d_fixed

זה כבר יותר הגיוני.

מה עושים עם זה

במשחק אמיתי כמובן לא נשתמש ב- Box2DDebugRenderer בשביל להציג. נשמור את המיקום של כל Body בו אנחנו משתמשים, ונצייר שם צורה/תמונה באותו מיקום (לא לשכוח להכפיל בחזרה ב- PPM!). בשלבי הפיתוח נשתמש בשתי מצלמות – אחת בשביל ה- Box2DDebugRenderer והשנייה לצורות האמיתיות – ואחרי שנראה שזה חופף, ״נכבה״ את הרינדור של הדיבאגר.

הנה דוגמא פשוטה לאפליקציה שמציגה המון כדורים שנופלים:

package com.yair.physics;
 
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.CircleShape;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.TimeUtils;
 
public class Box2dExample extends Game {
	private World world;
	// only for debug
//	private Box2DDebugRenderer b2dr;
//	private OrthographicCamera b2dCam
	private OrthographicCamera cam;
	private int SCREEN_WIDTH = 480;
	private int SCREEN_HEIGHT = 800;
	private float PPM = 100;
	private long lastSpawnTime;
 
	private Array<ColorBody> bodies = new Array<ColorBody>();
	private ShapeRenderer shapeRenderer;
 
	@Override
	public void create () {
		world = new World(new Vector2(0, 9.81f), false);
		// only for debug
//		b2dCam = new OrthographicCamera();
//		b2dCam.setToOrtho(true, SCREEN_WIDTH / PPM,
//				SCREEN_HEIGHT / PPM);
 
		cam = new OrthographicCamera();
		cam.setToOrtho(true, SCREEN_WIDTH,
				SCREEN_HEIGHT);
		// only for debug
//		b2dr = new Box2DDebugRenderer();
		shapeRenderer = new ShapeRenderer();
 
		createGround(new Rectangle(SCREEN_WIDTH / 2, SCREEN_HEIGHT, SCREEN_WIDTH, 10));
	}
 
	private void createBall(int x, int y){
		BodyDef bdef = new BodyDef();
		bdef.position.set(x / PPM, y / PPM);
		bdef.type = BodyDef.BodyType.DynamicBody;
		Body ball = world.createBody(bdef);
		CircleShape shape = new CircleShape();
		shape.setRadius(20f / PPM);
		FixtureDef fdef = new FixtureDef();
		fdef.restitution = 0.7f;
		fdef.density = 2f;
		fdef.friction = 0.25f;
		fdef.shape = shape;
		ball.createFixture(fdef).setUserData("ball");
		ball.setUserData(this);
		shape.dispose();
 
		bodies.add(new ColorBody(ball));
	}
 
	private void createGround(Rectangle rect){
		BodyDef bdef = new BodyDef();
		bdef.position.set(rect.getX() / PPM, rect.getY() / PPM);
		bdef.type = BodyDef.BodyType.StaticBody;
		Body ground = world.createBody(bdef);
		PolygonShape shape = new PolygonShape();
		shape.setAsBox(rect.getWidth() / PPM, rect.getHeight() / PPM);
		FixtureDef fdef = new FixtureDef();
		fdef.restitution = 0f;
		fdef.friction = 0.5f;
		fdef.shape = shape;
		ground.createFixture(fdef).setUserData("ground");
		ground.setUserData(this);
		shape.dispose();
	}
	@Override
	public void render () {
		Gdx.gl.glClearColor(0, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
		if ((TimeUtils.nanoTime() - lastSpawnTime)/10000 > 5000) {
			// spawn ball
			createBall( MathUtils.random(50, SCREEN_WIDTH - 50), 0);
			lastSpawnTime = TimeUtils.nanoTime();
		}
		world.step(Gdx.graphics.getDeltaTime(), 6, 2);
		// only for debug
//		b2dr.render(world, b2dCam.combined);
 
		// render ground
		shapeRenderer.setColor(Color.WHITE);
		shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
		shapeRenderer.rect(0, 0, SCREEN_WIDTH, 10);
		shapeRenderer.end();
 
		// iterate through balls array to render them
		for (ColorBody b : bodies) {
			shapeRenderer.setColor(b.color);
			shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
			shapeRenderer.circle(b.body.getPosition().x * PPM, 
SCREEN_HEIGHT - b.body.getPosition().y * PPM, 20);
			shapeRenderer.end();
		}
	}
 
	@Override
	public void dispose () {
		shapeRenderer.dispose();
	}
 
	// we want to save each ball with its specific color, so we use this struct
	private class ColorBody {
		public Body body;
		public Color color;
		public ColorBody(Body body) {
			this.body = body;
			this.color = getRandColor();
		}
		// generate random color from this list
		private Color getRandColor(){
			int rand = MathUtils.random(10);
			Color toRet;
			switch (rand) {
				case 0:
					toRet = Color.LIME;
					break;
				case 1:
					toRet = Color.RED;
					break;
				case 2:
					toRet = Color.YELLOW;
					break;
				case 3:
					toRet = Color.GREEN;
					break;
				case 4:
					toRet = Color.BLUE;
					break;
				case 5:
					toRet = Color.ORANGE;
					break;
				case 6:
					toRet = Color.DARK_GRAY;
					break;
				case 7:
					toRet = Color.MAGENTA;
					break;
				case 8:
					toRet = Color.MAROON;
					break;
				case 9:
					toRet = Color.GRAY;
					break;
				default:
					toRet = Color.WHITE;
					break;
			}
			return  toRet;
		}
	}
}

וזה נראה כך (זאת תמונה ב- Debug Mode, אפשר לראות את קווי המתאר של ה- Bodies, ואיך הם חופפים במדוייק את הציור):

balls

Two Cars

Two Cars

בהמשך לפוסט הזה, הפעם ניצור משחק.

בקצרה: libGDX היא ספרייה נוחה לשימוש עבור יצירת משחקי 2D. אפשר לראות בפוסט הנ״ל מדריך איך ליצור פרוייקט חדש ולהריץ אותו על פלטפורמות שונות.

ישנו משחק בשם Two Cars – צריך לשלוט בו זמנית על שתי מכוניות בלי להתנגש במכשולים. אני הולך ליצור גירסא שלו, עם שינוי קל. המשחק יראה בסופו של דבר ככה:

ezgif-2403319550

ניצור פרוייקט חדש בשם TwoCars. אין צורך ב- Box2D – לא נשתמש במנוע הפיזיקלי לצורך המשחק הזה.

נתחיל.

מבנה כללי

libGDX היא event-driven – כלומר האירועים השונים הם מה שמניעים אותנו. לדוגמא, בכל פעם שהמסך מתרפרש – תיקרא הפונקציה –

public void render(float delta)

 בה נכתוב מה אנחנו רוצים שיוצג על המסך. (מזכיר קצת את העבודה עם סקריפטים ב- Unity3D, שם העדכונים מגיעים לפונקציית update).

ניצור package חדש בשם helpers, וניצור בו שתי מחלקות – GameWorld, GameRenderer.

GameWorld – בה תהיה הלוגיקה של המשחק. היא תחזיק את ה- State של המשחק, המיקום בו נמצא כל אובייקט על המסך, וניקוד.

GameRenderer – אחראי לצייר על המסך את כל המידע שיש ב- GameWorld.

GameWorld:

public class GameWorld {
    public void update(float delta) {
 
    }
}

GameRenderer:

public class GameRenderer {
    private OrthographicCamera cam;
    private GameWorld gameWorld;
 
    public GameRenderer(GameWorld world){
        this.gameWorld = world;
 
        cam = new OrthographicCamera();
        cam.setToOrtho(true, GameConstants.SCREEN_WIDTH, GameConstants.SCREEN_HEIGHT);
    }
 
    public void render(){
 
    }
}

זה המבנה הבסיסי שיחזור על עצמו בכל משחק. בכל פריים חדש, נקרא ל- GameWorld כדי לעדכן את המצב במשחק, ואז נקרא ל- GameRenderer לעדכן את מה שמוצג.

ב- GameRenderer גם מאתחלים את המצלמה – היא אחראית על איך יראה כל מה שמוצג. נרחיב על כך בהמשך.

Read More Read More

libGDX 101

libGDX 101

libGDX היא ספרייה ליצירת משחקים. כתוב את המשחק פעם אחת (Java) ותוכל להריץ אותו פחות או יותר בכל פלטפורמה אפשרית: Android, iOS, Windows, MacOS, Linux, Web Browsers וכן הלאה.

יש ל- libGDX כלים נוחים לעבודה, מנוע פיזיקלי, והכי חשוב – קהילת מפתחים (זו ספריית קוד פתוח). מה שמאפשר את התמיכה ב- cross platform, זה שהספרייה מבוססת על OpenGL – שקיימת עבור כל הפלטפורמות הנ״ל (בשביל לרוץ בדפדפן יש את המקבילה, WebGL).

בפוסט הזה אני רוצה להדגים איך ליצור פרוייקט חדש ולהריץ אותו באנדרואיד, desktop, iOS ודפדפן.

יצירת פרוייקט

כדי ליצור פרוייקט חדש, כל מה שצריך זה להוריד מכאן קובץ JAR. אחרי שנפעיל את הקובץ, זה מה שנראה:

screen-shot-2016-09-14-at-10-55-28-am

נמלא את השדות כך:

Name: libGDX101

Package: com.yair.libgdx101

Game Class: LibGDX101

בשורה של ה- sub-projects נשאיר את התיבות איך שהן.

בשורה של ה- extensions נבטל את הבחירה ב- Box2D, מכיוון שלא נשתמש בדוגמא במנוע הפיזיקלי אז זה לא רלבנטי.

אחרי הלחיצה על Generate יווצר לנו פרוייקט Gradle במקום בו בחרנו.

Read More Read More