Tečaj
izdelave iger za mobilne telefone

:: Predavatelj: Žiga Hajduković. e-mail. .
:: Organizator: Dane Šoba. e-mail. Študentska organizacija FRI (ŠOFRI).

:: Domača stran tečaja: www.tetris1d.org/game/dev/tecaj/.
:: game/dev.si forum: www.tetris1d.org/game/dev/forum_si/.

:: Študentski info sistem FRI: http://www.student-info.net/fri/.

Novice

Čestitam vsem sodelujočim v velikem nagradnem tekmovanju za najboljšo mobilno igro, ki jo je sponzoriralo priznano podjetje Cocoasoft!
V zelo kratkem času vam je uspelo prekositi same sebe in ustvariti igre, ki so presegle vsa pričakovanja!
Objavili smo galerijo vseh sodelujočih iger, kjer si lahko ogledate posnetke akcije iz samih iger, pa tudi fotografije iz podelitve bogatih nagrad!
Ne pozabite na gd.si forum, kjer si bomo še naprej izmenjevali mnenja in pomagali drug drugemu na poti k izdelovanju najboljših mobilnih iger!

Lekcija 04

lekcija 03 :: domov :: lekcija 05

Epsilon 03

Epsilon 03 eclipse workspace project (sfx)

Epsilon 03 wap download

Odpri povezavo:

	http://www.tetris1d.org/gdwap/	
na svojem mobilnem telefonu in si naloži Epsilon_03.

Izvorna koda

Game.java (izvorna datoteka)

GameCanvas.java (izvorna datoteka)

GameSprite.java (izvorna datoteka)

Level.java (izvorna datoteka)

Grafika

Slika 1. Intro image

Slika 2. Background

Slika 3. Player

Slika 4. Izstrelki

Slika 5. Enemy sprite 1

Slika 6. Enemy sprite 2

Slika 7. Enemy sprite 3

Slika 8. Eksplozija

Slika 9. Tileset
Stage 1

Datoteka stage1.txt:


	public static final byte SPRITE_ID_PLAYER = 0;			// 'P'
	public static final byte SPRITE_ID_ENEMY1_SHOOTING = 1;		// 'A'
	public static final byte SPRITE_ID_ENEMY1 = 2;			// 'a'
	public static final byte SPRITE_ID_ENEMY2_SHOOTING = 3;		// 'V'
	public static final byte SPRITE_ID_ENEMY2 = 4;			// 'v'
	public static final byte SPRITE_ID_ENEMY3 = 5;			// 'O'
	public static final byte SPRITE_ID_BOSS1 = 6;			// 'B'



OOOOOOOOOOOOOOOO;                ;
OOvvvvvvvvvvvvOO;                ;
O/            \O;                ;
>              <;       B        ;
>              <;                ;
>              <;                ;
>              <;                ;
>              <;                ;
OL            JO;                ;
OGL          JGO;                ;
OOGL        JGOO;     V   V      ;
\OOGL      JGOO/;                ;
 \OG/      \GO/ ;                ;
  \/        \/  ;                ;
                ;                ;
       JL       ;     A   A      ;
      JOOL      ;                ;
L     <OOG      ;   O   O   O    ;
>     <GG>     J;                ;
/     <OO>     <;                ;
      GOO>     \;                ;
      \GG/      ;   V       V    ;
       \/       ;                ;
               J;                ;
    J^^L      JO;                ;
   JOvvOL     \O;                ;
  JG/  O>      \;      A A       ;
  <>  J>/      J;    A     A     ;
  J/  <G      JO;                ;
 J>  JO/     JOG;                ;
J/  JO/     JOGO;                ;
GO^^^/      OOOG;                ;
GOGG/       OOGO;                ;
OGOO        OGOO;     O    O     ;
vvvv        Ovvv;                ;
                ;                ;
  JL        JL  ;      V  V      ;
 JOOL      JOOL ;                ;
JOOGO      OGOOL;                ;
OOGO>      <OGOO; O            O ;
\GOGO      OGOG/;                ;
 OOO>      <OOO ;   O        O   ;
 vvvv      vvvv ;                ;
 OOOO      OOOO ;     O    O     ;
 \vv/      \vv/ ;                ;
                ;  O          O  ;
L              J;                ;
>             <O;                ;
/              \;                ;
                ;      a  a      ;
       JL       ;                ;
      JOOL      ;                ;
L     <OOG      ;  v          v  ;
>     <GG>     J;    v      v    ;
/     <OO>     <;      v  v      ;
      GOO>     \;                ;
      \GG/      ;                ;
       \/       ;                ;
               J;                ;
    J^^L      JO;                ;
   JOOOOL     \O;                ;
  JOOGOO>      \;                ;
  <OOOGO/      J;       P        ;
  JOOOOG      JO;                ;

Nalaganje sprite-ov iz stage1.txt

V loadLevel smo dodali še nalaganje nasprotnikov, njihovih položajev in tipov.

Pomemben je spriteData array, ki določa nekatere lastnosti (damage, energy, score) nasprotnikov, glede na njihov spriteId.

Nasprotnike kreiramo in dodamo v enemySprites seznam kar vse že v loadLevel, čeprav pa pazimo, da optimiziramo zanko, ki pregleduje seznam nasprotnikov v updateEnemySprites in podobnih metodah.


public static final byte SPRITE_DATA_INDEX_IMG_ID = 0;
public static final byte SPRITE_DATA_INDEX_MAX_ENERGY = 1;
public static final byte SPRITE_DATA_INDEX_DAMAGE = 2;
public static final byte SPRITE_DATA_INDEX_SCORE = 3;

// spriteData[sprite_id][data_index],  image_id, energyMax, damage, score
public static final short[][] spriteData = {
	{ GameCanvas.SPRITE_IMAGE_PLAYER, 200, 20, 0 },		//	SPRITE_ID_PLAYER
	{ GameCanvas.SPRITE_IMAGE_ENEMY1, 40, 30, 20 },		//	SPRITE_ID_ENEMY1
	{ GameCanvas.SPRITE_IMAGE_ENEMY1, 40, 30, 40 },		//	SPRITE_ID_ENEMY1_SHOOTING
	{ GameCanvas.SPRITE_IMAGE_ENEMY2, 20, 20, 10 },		//	SPRITE_ID_ENEMY3
	{ GameCanvas.SPRITE_IMAGE_ENEMY2, 20, 20, 20 },		//	SPRITE_ID_ENEMY2_SHOOTING
	{ GameCanvas.SPRITE_IMAGE_ENEMY3, 60, 40, 30 },		//	SPRITE_ID_ENEMY3
	{ GameCanvas.SPRITE_IMAGE_BOSS1, 200, 100, 100 },	//	SPRITE_ID_BOSS1
	{ GameCanvas.SPRITE_IMAGE_EXPLOSION, 0, 0, 0 },		//	SPRITE_ID_EXPLOSION
	{ GameCanvas.SPRITE_IMAGE_FIRE, 0, 20, 0 },			//	SPRITE_ID_FIRE1  // player fire
	{ GameCanvas.SPRITE_IMAGE_FIRE, 0, 20, 0 },			//	SPRITE_ID_FIRE2  // blue fire
	{ GameCanvas.SPRITE_IMAGE_FIRE, 0, 10, 0 },			//	SPRITE_ID_FIRE3  // green round bullet
};


// load a row of sprites
sprite_map_col = 0;
ch = ' ';
while (ch != ';')
{
	ch = (char) datainputstream.readByte();

	int level_x = sprite_map_col * TILE_SIZE;
	int level_y = map_row * TILE_SIZE;
	if (ch != ';')
	{
		byte sprite_id = getSpriteId(ch);
		switch (sprite_id)
		{
		case SPRITE_ID_PLAYER:

			// initialize player sprite
			GameCanvas.player = new GameSprite( SPRITE_ID_PLAYER );
							
			GameCanvas.player.x = level_x;
			GameCanvas.player.y = level_y;

			Level.tiles_scroll_x = GameCanvas.player.x + GameCanvas.player.w/2 - GameCanvas.canvasWidth/2;

			GameCanvas.player.frameId = GameCanvas.SPRITE_FRAME_PLAYER_CENTER;
			break;
            				
		case SPRITE_ID_ENEMY1:
       		case SPRITE_ID_ENEMY1_SHOOTING:
           	case SPRITE_ID_ENEMY2:
           	case SPRITE_ID_ENEMY2_SHOOTING:
            	case SPRITE_ID_ENEMY3:
            	case SPRITE_ID_BOSS1:

			// initialize player sprite
			new_enemy = new GameSprite( sprite_id );

			new_enemy.x = level_x;
			new_enemy.y = level_y;
							
			new_enemy.vx = 0;

			switch (sprite_id) {
            		case SPRITE_ID_ENEMY1:
                	case SPRITE_ID_ENEMY1_SHOOTING:
    				new_enemy.vy = 2;
                		break;
                	case SPRITE_ID_ENEMY2:
                	case SPRITE_ID_ENEMY2_SHOOTING:
    				new_enemy.vy = 1;
                		break;
                	case SPRITE_ID_ENEMY3:
    				new_enemy.vy = 3;
                		break;
                	case SPRITE_ID_BOSS1:
    				new_enemy.vy = 0;
    				new_enemy.vx = 1;
    					
    				// save a reference to boss sprite
    				boss = new_enemy;
                		break;
                	}

			enemySprites.addElement(new_enemy);
			break;
		}

		sprite_map_col++;

	}


Proženje, premikanje (in streljanje) nasprotnikov

Dodali smo ACTIVATION_ZONE spremenljivko, ki nam določa, koliko nad ekranom mora biti sprite, da se aktivira, t.j. da ga zaćne zanka v updateEnemySprites() upoštevati.

public void updateEnemySprites()
{
	// move enemy sprites
	int i = Level.enemySprites.size()-1;
	while (i >= 0)
	{
		GameSprite enemy = (GameSprite) Level.enemySprites.elementAt(i);
		
		if ( (enemy.y - Level.tiles_scroll_y > canvasHeight) )
		{
			// remove out-of-screen enemies
			Level.enemySprites.removeElementAt(i);
			i--;
		}
		else
		{
			if ( enemy.y - Level.tiles_scroll_y > Level.ENEMY_AI_ACTIVATION_ZONE_Y )
			{
				// do the AI move, shoot and other tricks
				doEnemyAI( enemy );
				i--;
			} else {
				// optimization
				// but enemySprites must be ordered by the sprites' y coordinate !
				// they are in this case, because of loadLevel
				break;
			}
		}
	}
}

Dodali smo preprost AI pogon za nasprotnike, skriva pa se v doEnemyAI() funkciji.

Nasprotniki znajo streljati, izstrelke hranimo v enemyFireSprites in jih poganjamo z updateEnemyFireSprites(). Seveda smo dodali še preverjanje prekrivanja (collision detection) player sprite-a z izstrelki nasprotnikov.

Implementirali smo zelo preprost AI za premikanje in streljanje Boss-a.


public void doEnemyAI( GameSprite sprite )
{
	switch (sprite.spriteId)
	{
	case Level.SPRITE_ID_ENEMY1:
		break;
	case Level.SPRITE_ID_ENEMY1_SHOOTING:

		shootEnemyFire(sprite, Level.SPRITE_ID_FIRE2, (byte) 1);
		break;
	case Level.SPRITE_ID_ENEMY2:
		break;
	case Level.SPRITE_ID_ENEMY2_SHOOTING:
			
		shootEnemyFire(sprite, Level.SPRITE_ID_FIRE2, (byte) 1);
		break;
	case Level.SPRITE_ID_ENEMY3:

		// animate sprite to next sprite frame every 3 frames
		if (global_frame_counter % 3 == 0)
			sprite.frameId = (byte) ( (sprite.frameId + 1) % (sprite.frameIdMax+1));
	
		break;
	case Level.SPRITE_ID_BOSS1:
		activate_boss_energy_bar = true;

		// boss fire
		if (sprite.fire_cooldown_frames_counter <= 0)
		{
			GameSprite new_fire;
			new_fire = new GameSprite(Level.SPRITE_ID_FIRE2);
			new_fire.frameId = 1;
			new_fire.x = sprite.x + sprite.w - 5 - new_fire.w;
			new_fire.y = sprite.y + sprite.h;
			new_fire.vy = sprite.vy + FIRE_VELOCITY/2;
			enemyFireSprites.addElement(new_fire);

			new_fire = new GameSprite(Level.SPRITE_ID_FIRE2);
			new_fire.frameId = 1;
			new_fire.x = sprite.x + 5;
			new_fire.y = sprite.y + sprite.h;
			new_fire.vy = sprite.vy + FIRE_VELOCITY/2;
			enemyFireSprites.addElement(new_fire);

			new_fire = new GameSprite(Level.SPRITE_ID_FIRE3);
			new_fire.frameId = 2;
			new_fire.x = sprite.x + sprite.w / 2 - new_fire.w / 2;
			new_fire.y = sprite.y + sprite.h;
			new_fire.vy = sprite.vy + FIRE_VELOCITY;
			enemyFireSprites.addElement(new_fire);

			sprite.fire_cooldown_frames_counter = ENEMY_FIRE_COOLDOWN_WAIT_FRAMES;
		} else {
			// extra cooldown, boss only
			sprite.fire_cooldown_frames_counter--;
		}
			
		// boss move
		if (sprite.x + sprite.w > (Level.LEVEL_WIDTH * Level.TILE_SIZE) - (Level.TILE_SIZE/2) )
			sprite.vx = -1;
		if (sprite.x < Level.TILE_SIZE/2 )
			sprite.vx = 1;
		
		break;
	}
		
	// move the sprite
	sprite.x += sprite.vx;
	sprite.y += sprite.vy;

	if (sprite.fire_cooldown_frames_counter > 0)
		sprite.fire_cooldown_frames_counter--;
}

Reagiranje na prekinitve (incoming calls, battery low warnings...)

Zelo pomembno je, da se pri razvoju iger za mobilne telefone zavedamo, da lahko pride do prekinitev, najbolj očitna bi bila npr. prejeti klic. V ta namen nam metodi razreda Canvas hideNotify() in showNotify pomagata zaznati te zunanje nepredvidljive dogodke.

V hideNotify() v naši igri aktiviramo Pause game screen, ki ima možnosti Resume in End game. (v ta ekran pridemo tudi z desno softkey tipko, ko smo v igri). Ko se igra znova aktivira na ekran, lahko igralec nadaljuje tam, kjer je ostal.

showNotify() nismo uporabili, rabili pa bi jo v primeru igranja glasbe v ozadju. Takrat bi uporabili showNotify() za ponovno aktiviranje glasbene spremljave.

public void hideNotify()
{
	// hideNotify method of Canvas is called on external events, which hide the Canvas
	// this method can be used to handle such events (battery warnings, incoming phone calls...)
	if (mode == MODE_GAME)
	{
		mode = MODE_MENU;
		menuScreen = MENU_SCR_PAUSE_GAME;
	}
}

Highscore

Shranjevanje podatkov, npr. highscore spiska, naredimo s pomočjo RMS. To je interna baza telefona, ki temelji na sistemu zapisov oz. record-ov. Vsak MIDlet lahko kreira več RMS baz (predstavljamo si jih lahko tudi kot tabele), vsaka pa lahko vsebuje več record-ov, ki so pravzaprav le byte array-i. S pomočjo DataInputStream lahko torej "pakiramo" podatke v/iz RMS baze. Vsak rekord ima svoj recordId, ki predstavlja ključ (primary key) za zapise. Prvi zapis dobi recordId = 1, pri naslednjih se povečuje za 1.

V našem primeru bomo uporabili eno RMS bazo in vseh 5 highscore imen in točk shranili v en record.

Ko se igra zažene (MODE_INTRO), naložimo highscore spisek (loadHighscoreFromRMS(), hiNames, hiScores) iz RMS oz. inicializiramo RMS na privzete zapise.

Implementirali smo nove ekrane (MENU_SCR_HISCORE_VIEW, MENU_SCR_HISCORE_ENTER) in prehode med njimi (v run(). case MODE_MENU).

Po koncu igre kličemo metodo isQualified(score), da preverimo, če se igralec kvalificira za vpis na lestvico najboljših 5. Če se, odpremo HISCORE_ENTER ekran, kjer ima igralec možnost vpisati svoje ime. Znake smo omejili na 'A' do 'Z', dolžino imena pa na 3 znake.

Po vpisu imena z addHighscore(int score, String name) shranimo zapis. Pomožna funkcija getHighscoreData() nam "zapakira" podatke v byte array, ki ga zapišemo v 1. zapis naše RMS baze. Funkcija saveHighscores() pa zapiše trenutno stanje v seznamih hiNames[] in hiScores[] v RMS bazo.


// loads the highscores from RMS - the internal phone database
// also saves default scores, if no scores exist.
public static void loadHighscoreFromRMS()
{
	try
	{
		db = RecordStore.openRecordStore(SCORE_DB_NAME, true);
		byte[] score_data = null;
		// initialize scores
		if (db.getNumRecords() == 0)
		{
			// add record with initial default scores
			score_data = getHighscoreData();
			db.addRecord(score_data, 0, score_data.length);
		}
		// load highscore
		score_data = db.getRecord(1);
		ByteArrayInputStream in = new ByteArrayInputStream(score_data);
		DataInputStream dis = new DataInputStream(in);
		for (byte i = 0; i < MAX_SCORES; i++)
		{
			hiScores[i] = dis.readInt();
			hiNames[i] = dis.readUTF();
		}
		dis.close();
		in.close();
		db.closeRecordStore();
		dis = null;
		in = null;
		db = null;
	} catch (Exception ex) {
		ex.printStackTrace();
	}
}

Namig: Kako "počistimo" RMS zapise na emulatorju

RMS baze se nahajajo v poddirektoriju emulatorja.

Za Nokia S60 emulator:
...devices\Series_60_MIDP_Concept_SDK_Beta_0_3_1_Nokia_edition\appdb\*.db

Za WTK22/DefaultColorPhone:
\wtk22\appdw\DefaultColorPhone

Preprosto pobrišemo ustrezno .db datoteko.

O sprite x in y koordinatah

Sprite-i so zdaj prilepljeni na level, njihove koordinate so zdaj relativne na level, ne na canvas. To pomeni, da moramo player sprite-u pristevati vy premikanja konstrukcije, da ne bi "zaostajal".

	public void updatePlayerSprite()
	{
		// move player with level scroll
		player.y += Level.tiles_scroll_vy;

Ostale novosti

Cooldown_counter za ohlajanje topa smo premaknili v GameSprite, saj ima zdaj vsak sprite možnost strealjanja.

Podobno kot za igralca smo tudi za Boss-a dodali veliko eksplozijo po njegovem slavnostnem pokončanju. Glej BOSS_DEAD_ spremenljivke.

Če nasprotnik ni pokončan, začnemo eksplozijo šele na EXPLOSION_FRAME_SMOKE_START frame-u. Glej metodo addExplosion().

Še en način poganjanja glavne zanke pogona igre:

Poleg izvajanja glavne zanke v Thread objektu, poznamo še "callSerially() način". Klicem metode (običajno po klicu repaint()) midlet.display.callSerially(this), doda klic run() funkcije objekta (this, ki mora biti Runnable), na sklad izvajanja funkcij.

V ta namen moramo spremeniti while(gameRunning) zanko v if(gameRunning), sa se nam run() vsakič izvrši, nato pa spet zažene. Dodati moramo še klic na callSerially(), v konstrukciji MIDlet objekta pa ne kreiramo več Thread objekta za naš Canvas, ampak direktno zaženemo run() metodo.

Space-shooter-vertical-scroller material

Epsilon, dodatna grafika (by geci)

Grafika za igro Axion Space, Cocoasoft

Naloge

  1. Dodaj nasprotnika ENEMY3_SHOOTING, ki bo streljal zelene metke v smeri igralca.
  2. Player warp effect. Po uničenju boss-a naj player sprite "aktivira" warp drive.
    Npr. nariši vertikalno zaporedno slike player sprite-a. Efekt fade za slikce, ki "ostanejo zadaj" lahko simuliraš z risanjem črnih črt čez sliko, npr. vsako drugo in/ali vsako tretjo vrstico.
  3. Level design. Poskušaj razporediti nasprotnike tako, da jih bo možno vse uničiti, hkrati pa to ne bo to. Pazi tudi na to, da se bo zahtevnost stopnjevala proti koncu stopnje.
  4. Naj bo Boss samo na nekaterih delih "občutljiv" za zadetke.


game/dev.si :: predlogi ? pišite: Žiga Hajduković