どんなゲームにするか。
ゲーム内容(コンセプト)を一言でいうと、ワンボタンで遊べる避けゲー。
ざっくり言うと、シューティングゲームから弾を避ける要素のみにしたような感じ。
ワンボタンで遊べるという手軽さから、ガラケーでの操作にも似合い、制限された環境でも動作させられるという点がポイント。
個人的に開発したことのないプラットフォームでの開発を経験してみたいと感じ、敢えてガラケーのJavaというプラットフォームをチョイスした。
ゲーム仕様
ボタンを押している間キャラクターは右に移動。離すと左に戻っていく。
オブジェクトはランダムに降ってくる。
オブジェクトにキャラクターが触れるとライフが減少する。
UIとして、進行状況を表すバーとスコア表示を常に画面に表示
スコアが上昇している間には表示とSEの再生を入れる。
避けるだけのゲーム内容で単調だったので何かしら要素を追加 →降ってくるオブジェクトに接近することで得点が増加するように変更。ギリギリのスリルが味わえます(?)*1
ストーリー面 →クリアごとにタイトル画面が変わる
ゲーム内容自体はこんなシンプルなルールしかありません。元々初めて作ったアクション系ゲームなので面白いかどうかはともかくプログラムとしての想像が沸きやすい、といった点を重視した所このようなゲームに。
メイン関数内の解説。
run()を実行してwhileに入るまでの長々とした部分でゲーム内の変数を生成しています。
whileに入ってからはswitch文で分岐させています。遷移を図にしてみるとこのようになっています。
図にしてみるともっとわかりやすい分岐ありそうだと思うんですよね…ゲームはまだ見よう見まねで作ってるので詳しい人教えてください!
コードを見てみると、switch文の各caseに処理と描画メソッドの呼び出しが入っており、処理に続いて描画メソッドの呼び出しが入っています。
メモリが限られた環境でパフォーマンスを意識する場合はあまり関数分けしない方が良いと聞いたので*2、描画部分や長くなっている部分以外はそのままにしたため、このようになっています。
ソースコード
呼び出し部分。Y!mobileのサンプルを参考に。
import javax.microedition.lcdui.*; import javax.microedition.midlet.*; public class Teruteru extends MIDlet { static MIDlet midlet; //コンストラクタ public Teruteru() { midlet=this; TeruteruCanvas sc = new TeruteruCanvas(); Display.getDisplay(this).setCurrent(sc); Thread thread=new Thread(sc); thread.start(); } //アプリの開始 public void startApp() { } //アプリの一時停止 public void pauseApp() { } //アプリの終了 public void destroyApp(boolean flag) { } }
メインの部分。
import java.util.Random; import javax.microedition.lcdui.*; import javax.microedition.midlet.*; import javax.microedition.lcdui.game.*; import javax.microedition.media.*; import javax.microedition.media.control.*; import java.io.*; public class TeruteruCanvas extends GameCanvas implements Runnable,CommandListener{ //画像 Image im_title; Image im_Player; Image im_ame; Image im_bg; Image im_gover; //BGM Player sndBGM; Player goverBGM; Player damageSE; Player scoreSE; String BGMname; String SEname; //ディスプレイサイズ取得 int display_width = getWidth(); int display_height = getHeight(); //ランダム生成 Random random = new Random(); //ソフトキー Command exit; Command inst; String keyCommand = ""; //コマンド保存 boolean inst_flag = false; boolean flag; //コンストラクタ TeruteruCanvas(){ //キーイベントの抑制 super(false); //画像の読み込み try{ im_title = Image.createImage("/teruri-1.png"); im_Player = Image.createImage("/player_sp.png"); im_ame = Image.createImage("/ame.png"); im_bg = Image.createImage("/bg.png"); im_gover = Image.createImage("/gameover.png"); }catch(Exception e){ System.out.println(e.getClass().getName()); } //BGMの読み込み try { InputStream in_bgm = getClass().getResourceAsStream("/teruteru_pops.mid"); InputStream in_gover = getClass().getResourceAsStream("/teruteru_Healing_Harp.mid"); InputStream in_damage = getClass().getResourceAsStream("/se_damage.wav"); InputStream in_score = getClass().getResourceAsStream("/se_score.wav"); sndBGM = Manager.createPlayer(in_bgm, "audio/midi"); goverBGM = Manager.createPlayer(in_gover, "audio/midi"); damageSE = Manager.createPlayer(in_damage, "audio/x-wav"); scoreSE = Manager.createPlayer(in_score, "audio/x-wav"); sndBGM.setLoopCount(-1); }catch (Exception e) { e.printStackTrace(); } //ソフトキーの設定 exit = new Command("終了", Command.EXIT, 0); addCommand(exit); inst = new Command("操作説明", Command.SCREEN, 1); addCommand(inst); setCommandListener(this); } //雨粒オブジェクトクラス //落ちてくるオブジェクトの座標の生成、移動、生存の情報を持っているクラス。オブジェクトを生成するとランダムで座標を決定します。 class Object_Ame { //雨粒初期位置 private int x; private int y; //雨粒画像サイズ 現状は直入力 private int ame_width; private int ame_height; //雨粒移動距離 private int dy; //画面内生存判定 private boolean suv; private int rnd; //コンストラクタ Object_Ame(){ Set(); this.ame_width = 32; this.ame_height = 5; this.dy = 5; this.suv = true; } int getX(){return x;} int getY(){return y;} int getWidth(){return ame_width;} int getHeight(){return ame_height;} boolean getState(){return suv;} //xy軸決定 void Set(){ setX(randX()); setY(randY()); } void setX(int setx){ x = setx; } void setY(int sety){ y = sety; } //ランダム int randX(){ int field_width = display_width/10; rnd = random.nextInt(display_width) - field_width - ame_width*2; if(rnd < 0) randX(); return rnd; } int randY(){ int field_height = display_height; rnd = (random.nextInt(display_height) - field_height) + 50; //if(rnd < 0) randY(); return rnd; } //dy方向移動 void MoveAme(){ y += dy; } //死亡判定 void deadState(){ suv = false; } } //プレイヤーに関する情報と制御を行うクラス。inv_checkとisinvの役割がかぶってる気がしますが、inv_checkはインクリメントしてるのでコード中1回はこっちを呼び出すんだと思う()多分もっとうまくやれる class Object_Player { private int life; private int p_width; //プレイヤースプライトサイズ //プレイヤー初期位置 private int x; private int y; //移動速度 private int dir; //移動方向 private int dx; //通常移動速度 //無敵時間 private int inv; //無敵時間 private int inv_f; //無敵経過時間 //接近フラグ private boolean close; //点滅管理用 private int visflag; Object_Player(){ this.life = 4; this.p_width = 40; this.x = 0; this.y = display_height * 4/ 5; this.dir = 0; //移動方向 this.dx = 4; //通常移動速度 this.inv = 25; //無敵時間 this.inv_f = 0; //無敵経過時間 this.visflag = 0; this.close = false; } int getX(){return x;} int getY(){return y;} int getdir(){return dir;} int getinv(){return inv;} int getinv_f(){return inv_f;} int getlife(){return life;} int getvisflag(){return visflag;} boolean getclose(){return close;} void PlayerControl(int key){ //ボタン進行判定 if (key != 0){ setdir(1); }else if(getX() > 0){ setdir(-1); }else{ setdir(0); } PlayerMove(); } void PlayerMove(){ x += dx * dir; } void setdir(int setDir){ dir = setDir; } void setinv_f(int sett){ inv_f = sett; } void inv_check(){ if(inv_f >= inv){ setinv_f(0); }else if(isinv()){ inv_f++; } } boolean isinv(){ if(inv_f >= 1 & inv_f < inv){ return true; } return false; } void lifedown(){ life--; } int switchvisflag(){ if(visflag == 0){ visflag = 1; }else if(visflag == 1){ visflag = 0; } return visflag; } void setclose(boolean st){ close = st; } void spacehit(){ int field_width = display_width/10; if(x < 0) x = 0; if(x > display_width - p_width - field_width) x = display_width - p_width - field_width; } } // ステージ制御 class Stage { private int stagesize; //ステージ長さ private int progress; //進行状況 private int score; //スコア //画面内の雨粒数 private int ame_st; private int ame_max; private int framecount; //フレームカウント Stage(){ this.stagesize = 1000; //ステージ長さ this.progress = 0; //進行状況 this.score = 0; //スコア //画面内の雨粒数 this.ame_st = 7; this.ame_max = 7; this.framecount = 0; } int getProgress(){return progress;} int getStage(){return stagesize;} int getScore(){return score;} int getAmest(){return ame_st;} int getAmemax(){return ame_max;} int getFramecount(){return framecount;} void incProgress(){ progress++; } void incScore(){ score++; } boolean StageEnd(){ if(progress > stagesize){ return true; } return false; } void count_up(){ framecount++; } } // あたり判定などの制御 class Sqer { private int x; private int y; private int w; private int h; Sqer(int sq_x, int sq_y, int sq_w, int sq_h){ this.x = sq_x; this.y = sq_y; this.w = sq_w; this.h = sq_h; } int getX(){return x;} int getY(){return y;} int getW(){return w;} int getH(){return h;} int getCX(){ return (x+w)/2; } int getCY(){ return (y+h)/2; } } public void run(){ Graphics g = getGraphics(); int scene = 1; //シーン情報 //ゲームグラフィック Sprite title = new Sprite(im_title, 250, 250); Sprite p = new Sprite(im_Player, 50, 50); Sqer p_cl = new Sqer(16, 4, 18, 19); p.defineCollisionRectangle(p_cl.getX(), p_cl.getY(), p_cl.getW(), p_cl.getH()); Sprite bg = new Sprite(im_bg); Sprite gover = new Sprite(im_gover); //ステージ生成 Stage s = new Stage(); //配列変数 Object_Ame[] ame = new Object_Ame[s.getAmemax()]; Sprite[] am = new Sprite[s.getAmemax()]; Sqer am_cl = new Sqer(2,11,12,12); for(int i = 0; i < s.getAmemax(); i++){ am[i] = new Sprite(im_ame); am[i].defineCollisionRectangle(am_cl.getX(), am_cl.getY(), am_cl.getW(), am_cl.getH()); } //プレイヤー生成 Object_Player player = new Object_Player(); //ゲージ用変数設定 Sqer g1 = new Sqer(display_width - display_width/10, display_height/10, display_width/25, (display_width * 3)/5); //Font Font largefont; Font font; largefont = Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_PLAIN,Font.SIZE_LARGE); font = Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_PLAIN,Font.SIZE_MEDIUM); int clear = 0; //クリア回数 int high_score = 0; //ハイスコア int keyState = 0; //キー情報 flag = true; while(flag){ //画面の初期化 g.setColor(255,255,255); g.fillRect(0,0,display_width,display_height); //ボタン操作取得 keyState = getKeyStates(); switch(scene){ case 0: //init player = new Object_Player(); s = new Stage(); //配列初期化 ame = new Object_Ame[s.getAmemax()]; //コマンド初期化 keyCommand = ""; scene = 1; break; case 1: //タイトル画面 //スタートキーが押された if((FIRE_PRESSED &keyState) != 0){ scene = 2; //BGMスタート try{ sndBGM.start(); sndBGM.setMediaTime(0); //最初から再生します }catch (Exception e) { e.printStackTrace(); } } //操作説明キーが押された if(inst_flag == true){ scene = 5; } drawTitle(g, font, largefont, title, high_score, clear); break; case 2: //ゲームループ //ボタン進行判定 player.PlayerControl(FIRE_PRESSED &keyState); //移動制限 player.spacehit(); //当たり判定 GameisHit(ame, am, player, p, s, am_cl, p_cl, s.getAmest()); //無敵継続処理 player.inv_check(); //進行増加 s.incProgress(); s.count_up(); //終了判定 if(s.StageEnd()){ scene = 4; try{ sndBGM.stop(); }catch (Exception e) { e.printStackTrace(); } }else if(player.getlife() <= 0){ scene = 3; try{ sndBGM.stop(); }catch (Exception e) { e.printStackTrace(); } } //隠しコマンド if(keyCommand.equals("6109")){ player.setinv_f(1); }else if(keyCommand.equals("37564")){ player.lifedown(); } //描画 drawGameBackground(g, bg); drawGamePlayer(g, player, p, s.getFramecount()); drawGameMoveObj(g, ame, am, s.getAmest()); drawGameUI(g, font, largefont, s.getScore(), player.getlife(), player.getclose(), s, g1); break; case 3: //ゲームオーバー画面 //BGM try{ goverBGM.start(); }catch (Exception e) { e.printStackTrace(); } //スコアを更新 if(s.getScore() > high_score){ high_score = s.getScore(); } //描画 drawGameOver(g, font, gover, s.getScore()); try { Thread.sleep(500); } catch (Exception e){ } if((FIRE_PRESSED &keyState) != 0){ scene = 0; try{ goverBGM.stop(); goverBGM.setMediaTime(0); }catch (Exception e) { e.printStackTrace(); } } break; case 4: //クリア画面 //スコアを更新 if(s.getScore() > high_score){ high_score = s.getScore(); } //描画 drawClearStage(g, font, s.getScore(), player.getlife(), bg); try { Thread.sleep(1000); } catch (Exception e){ } if((FIRE_PRESSED &keyState) != 0){ scene = 0; clear++; } break; case 5: //操作説明 if(inst_flag == false){ scene = 1; } //描画 Instraction(g, font); break; } //画面に反映 flushGraphics(); //スリープ try { Thread.sleep(100); } catch (Exception e){ } } //アプリ終了通知 Teruteru.midlet.notifyDestroyed(); } public void keyPressed(int keyCode) { if (keyCode==0){ return; } switch(keyCode) { //数字キー・記号キー case KEY_NUM0: keyCommand+="0";break; case KEY_NUM1: keyCommand+="1";break; case KEY_NUM2: keyCommand+="2";break; case KEY_NUM3: keyCommand+="3";break; case KEY_NUM4: keyCommand+="4";break; case KEY_NUM5: keyCommand+="5";break; case KEY_NUM6: keyCommand+="6";break; case KEY_NUM7: keyCommand+="7";break; case KEY_NUM8: keyCommand+="8";break; case KEY_NUM9: keyCommand+="9";break; case KEY_STAR: keyCommand ="";break; } } public void GameisHit(Object_Ame[] ame, Sprite[] am, Object_Player player, Sprite p, Stage s, Sqer am_cl, Sqer p_cl, int ame_st){ //接近変数 int px2=0; int px1=0; int py2=0; int py1=0; int r = 40; int close_count = 0; //オブジェクト処理 for(int i = 0; i < ame_st; i++){ //オブジェクト生成 if(ame[i] == null || ame[i].getState() == false){ ame[i] = new Object_Ame(); }else{ //オブジェクト移動 ame[i].MoveAme(); } //範囲外に出たときの処置 if(ame[i].getY() > display_height){ ame[i].deadState(); } //衝突判定 if(player.getinv_f() == 0){ //無敵でないとき if(ame[i].getState() & p.collidesWith(am[i], true)){ //AmeがDeadでない&衝突している //SE再生 try{ damageSE.start(); damageSE.setMediaTime(0); //最初から再生します }catch (Exception e) { e.printStackTrace(); } player.lifedown(); ame[i].deadState(); player.setinv_f(1); break; } //かすり判定 px2 = am[i].getX() + am_cl.getCX() -r/2; py2 = am[i].getY() + am_cl.getCY() -r/2; px1 = player.getX() + p_cl.getCX() -r/3; py1 = player.getY() + p_cl.getCY() -r/2; if(Math.abs(px2 - px1) * Math.abs(px2 - px1) + Math.abs(py2 - py1) * Math.abs(py2 - py1) < r*r){ //接近スコア加算 try{ scoreSE.start(); }catch (Exception e) { e.printStackTrace(); } close_count++; //接近カウント s.incScore(); } } } //1つ以上と接近でフラグON if(close_count > 0){ player.setclose(true); }else{ player.setclose(false); } } public void drawGameBackground(Graphics g, Sprite bg){ //背景 bg.setFrame(0); bg.setPosition(0, 0); bg.paint(g); } public void drawGamePlayer(Graphics g, Object_Player player, Sprite p, int framecount){ //プレイヤーキャラ if(player.getdir() == 1){ p.setTransform(2); p.setFrame(1); }else if(player.getdir() == -1){ p.setTransform(0); p.setFrame(1); }else{ p.setFrame(0); } //点滅演出 if(player.isinv()){ if(player.switchvisflag()==0){ p.setVisible(false); }else{ p.setVisible(true); } }else{ p.setVisible(true); } //プレイヤー描画 int p_x = player.getX(); int p_y = player.getY(); p.setPosition(p_x, p_y); p.paint(g); } public void drawGameMoveObj(Graphics g, Object_Ame[] ame, Sprite[] am, int ame_st){ //障害物オブジェクト for(int i = 0; i < ame_st; i++){ int am_x = ame[i].getX(); int am_y = ame[i].getY(); am[i].setPosition(am_x, am_y); am[i].paint(g); } } public void drawGameUI(Graphics g, Font font, Font largefont, int score, int life, boolean close, Stage s, Sqer g1){ //進行ゲージ描画 int x1 = g1.getX(); int y1 = g1.getY(); int w1 = g1.getW(); int h1 = g1.getH(); int progress = s.getProgress(); int stage = s.getStage(); g.setColor(0,0,0); //影 g.drawRect(x1, y1, w1, h1); g.setColor(255,0,0); //実ゲージ g.fillRect(x1, y1, w1, h1); int gh_f = (int)((double)(1-((double) progress / stage)) * 100 * ((double)h1 / 100)); g.setColor(0,128,128); //減る部分 g.fillRect(x1 + 1, y1 + 1, w1 - 2, gh_f - 2); //スコア表示描画 g.setColor(0,0,0); g.setFont(largefont); g.drawString("ENERGY:" + score, 0, 0, g.LEFT | g.TOP); //接近演出 if(close){ g.setColor(255,0,0); g.drawString("+ENERGY", 70, (s.getFramecount() % 3), g.LEFT | g.TOP); } //ライフ表示描画 g.setColor(255,0,0); g.setFont(font); for(int i = 0; i < life; i++){ g.drawString("●", 0 + (i * 14), 25, g.LEFT | g.TOP); } } public void drawTitle(Graphics g, Font font, Font largefont, Sprite title, int high_score, int clear){ //描画 title.setFrame(0); title.setPosition(0, 0); title.paint(g); title.setPosition(0, 0); if(clear >= 3){ title.setFrame(3); title.paint(g); }else if(clear >= 2){ title.setFrame(1); title.paint(g); title.setFrame(2); title.paint(g); }else if(clear >= 1){ title.setFrame(1); title.paint(g); } g.setColor(0,0,0); g.setFont(largefont); g.drawString("HI SCORE:" + high_score, 0, 0, g.LEFT | g.TOP); g.drawString("PRESS START", display_width/3, display_height*4/5, g.LEFT | g.BOTTOM); g.setFont(font); g.drawString("★"+keyCommand, 0, display_height - 35, g.LEFT | g.TOP); g.drawString("(c) sui-kiki", 0, display_height, g.LEFT | g.BOTTOM); } public void drawGameOver(Graphics g, Font font, Sprite gover, int score){ //描画 g.setColor(0,0,0); g.fillRect(0,0,display_width,display_height); gover.setFrame(0); gover.setPosition(0, 0); gover.paint(g); g.setColor(255,255,255); g.setFont(font); g.drawString("Game Over", display_width/5, display_height*3/5+5, g.LEFT | g.TOP); g.drawString("Stage Score(ENERGY):" + score, display_width/5, display_height*3/5 + 20, g.LEFT | g.TOP); g.drawString("Restart PRESS BUTTON", display_width/3, display_height, g.LEFT | g.BOTTOM); } public void drawClearStage(Graphics g, Font font, int score, int life, Sprite bg){ drawGameBackground(g, bg); g.setColor(255,0,0); g.setFont(font); g.drawString("Game Clear!!", display_width/4, display_height/2 - 10, g.LEFT | g.TOP); g.setColor(0,0,0); g.drawString("Stage Score(ENERGY):" + score, display_width/4, display_height/2 + 15, g.LEFT | g.TOP); g.drawString("Life Bonus(Life*50):" + life * 50, display_width/4, display_height/2 + 30, g.LEFT | g.TOP); g.drawString("Final Score:" + score + (life * 50), display_width/4, display_height/2 + 45, g.LEFT | g.TOP); g.drawString("Restart PRESS BUTTON", display_width/4, display_height*4/5, g.LEFT | g.TOP); } public void Instraction(Graphics g, Font font){ //操作説明 g.setFont(font); g.setColor(200,200,0); g.drawString("★操作方法★", 0, 0, g.LEFT | g.TOP); g.setColor(0,0,0); g.drawString("中央キーで右方向に移動し、", 0, 15, g.LEFT | g.TOP); g.drawString("離すと元の位置に戻っていきます。", 0, 30, g.LEFT | g.TOP); g.setColor(200,200,0); g.drawString("★得点の増やし方★", 0, display_height/5, g.LEFT | g.TOP); g.setColor(0,0,0); g.drawString("雨粒に近づくとエネルギーを吸収し、", 0, display_height/5+15, g.LEFT | g.TOP); g.drawString("スコアが増加します。", 0, display_height/5+30, g.LEFT | g.TOP); g.setColor(200,200,0); g.drawString("★隠しコマンド★", 0, display_height*2/5, g.LEFT | g.TOP); g.setColor(0,0,0); g.drawString("いろいろためそう", 0, display_height*2/5+15, g.LEFT | g.TOP); g.setColor(200,200,0); g.drawString("★クリア回数によって", 0, display_height*3/5, g.LEFT | g.TOP); g.drawString(" タイトルイラストが変化します★", 0, display_height*3/5+15, g.LEFT | g.TOP); g.setColor(200,200,0); g.drawString("★使用した素材★", 0, display_height*4/5 , g.LEFT | g.TOP); g.setColor(0,0,0); g.drawString("BGM:音楽研究所", 0, display_height*4/5+15 , g.LEFT | g.TOP); g.drawString("SE:TAM Music Factory", 0, display_height*4/5+30 , g.LEFT | g.TOP); } //キーリリースイベント public void keyReleased(int keyCode) {} public void commandAction(Command c, Displayable d) { if (c == exit) { flag=false; }else if(c == inst){ //ボタン情報を送信 inst_flag = !inst_flag; } } }
関数一覧
case 1内
drawTitle(g, font, largefont, title, high_score, clear); //タイトル画面描画メソッド タイトル画面で表示するフォント、スプライト、ハイスコア //タイトル画面変更制御のフラグ(クリア回数)を入力
case 2内
player.PlayerControl(FIRE_PRESSED &keyState); //ボタン進行判定 player.spacehit(); //移動制限 GameisHit(ame, am, player, p, s, am_cl, p_cl, s.getAmest()); //当たり判定 プレイヤーとオブジェクトの座標を入力 player.inv_check(); //無敵継続処理 s.incProgress(); //進行増加 s.count_up(); //カウンタ増加
GameisHit以外はクラスメソッドとなっている。
drawGameBackground(g, bg); //背景描画 drawGamePlayer(g, player, p, s.getFramecount()); //プレイヤー描画 Framecountはダメージ受けた時の点滅描画のために使用。 drawGameMoveObj(g, ame, am, s.getAmest()); //オブジェクト描画 オブジェクト座標とスプライト情報とオブジェクト生存状況を入力 drawGameUI(g, font, largefont, s.getScore(), player.getlife(), player.getGraze(), s, g1); //UI描画 フォント情報とスコア、ライフ、かすり、進行状況、四角の情報を入力
画像の位置関係の問題があるので上に表示したいものを最後に描画
case 3内
drawGameOver(g, font, gover, s.getScore());
//画面描画メソッド ゲームオーバー画面で表示するフォント、スプライト情報、スコア
case 4内
drawClearStage(g, font, s.getScore(), player.getlife(), bg);
//画面描画メソッド クリア画面で表示するフォント、スコア、ライフ、背景
case 5内
Instraction(g, font);
//draw抜けてるけど描画メソッド フォント情報
*1:某シューティングゲームのシステムを参考にした。
*2:関数呼び出しにスタックを使用するのでそれがメモリに効いてくるとのこと。