Tečaj
:: 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/.
Č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 eclipse workspace project (sfx)
Odpri povezavo:
http://www.tetris1d.org/gdwap/na svojem mobilnem telefonu in si naloži Epsilon_03.
GameCanvas.java (izvorna datoteka)
GameSprite.java (izvorna datoteka)
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; ;
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++;
}
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--;
}
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;
}
}
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();
}
}
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.
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;
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.
Epsilon, dodatna grafika (by geci)
Grafika za igro Axion Space, Cocoasoft
| game/dev.si | :: predlogi ? pišite: Žiga Hajduković |