2012年11月5日月曜日

ステートマシン

ゲーム開発でよく使うステートマシンクラス。
いつも使っているソースコードを載せておきます。

/**
 * ステートマシンで取り扱われる状態を表すクラス
 * 
 * @author munomura
 * @version $Id: State.java 181 2010-04-25 15:23:58Z nomusanjp $
 */
public abstract class State {
    public void enter(O owner){}
    public void execute(O owner){}
    public void exit(O owner){}
}

/**
 * ゲームのNPCなどステートマシンを表すクラス
 * 
 * @author munomura
 * @version $Id: StateMachine.java 181 2010-04-25 15:23:58Z nomusanjp $
 */
public class StateMachine{
    public O own;
    public State global;
    public State current;
    public State previous;
    boolean isFirstUpdate=true;
    
    public StateMachine(O own){
        this.own = own;
    }
    
    public void setCurrentState(State s){current = s;}
    public void setGlobalState(State s){global = s;}
    public void setPreviousState(State s){previous = s;}
    public State getCurrentState(){return current;}
    public State getGlobalState(){return global;}
    public State getPreviousState(){return previous;}

    public void update() {
        if(isFirstUpdate){
            if(global != null)global.enter(own);
            if(current != null)current.enter(own);
            isFirstUpdate = false;
        }else{
            if(global != null)global.execute(own);
            if(current != null)current.execute(own);
        }
    }
    
    public void changeState(State newState){
        if(newState == null)return;
        
        if(global  != null)global.exit(own);
        if(current != null)current.exit(own);
        
        previous = current;
        current  = newState;
        
        if(global  != null)global.enter(own);
        if(current != null)current.enter(own);
        isFirstUpdate = false;
    }
    
    public void revertToPreviousState(){
        changeState(previous);
    }

    boolean isInState(State st){
        return st.equals(current);
    }
    
    /**
     * グローバルと初期ステートを同時に設定する。
     * AI初期化時に使用。
     * 
     * @param global
     * @param firstState
     */
    public void setupState(State global, State firstState){
        isFirstUpdate = true;
        this.global = global;
        this.current = firstState;
    }
}



public class Dog extends NPC implements Recycleable {

    protected StateMachine stateMachine = new StateMachine(this);

    @Override
    public void updateState(){
        stateMachine.update();
    }
}

public class DogRound extends State {
    public static DogRound state = new DogRound();

    @Override
    public void enter(Dog own){
    }

    @Override
    public void execute(Dog own){
    }
    
    @Override
    public void exit(Dog own){
    }
}

2012年4月30日月曜日

新作アプリ「アクマズフィールド」をリリースしました。このゲームは開発に2年以上かかっており、かなり大変でした。関係者の皆様、ご協力ありがとうございました。

このゲームは簡単にいうとピク□ンのようにAIの群れを操作しながら戦うアクションゲームです。同時にかなりの数のAIをスムーズに制御しなければいけなかったので、各種作り込みが大変でした。今思えばJavaでこのタイプのゲームを作るのは無茶だったなぁとしみじみ感じます。iPhone版を出すときはネイティブで記述できるので、もっとAIを同時に出しても処理出来そうな気がします。


今後は技術的なトピックをブログに追記していこうかなぁと考えていますのでご期待ください。

2012年2月21日火曜日

計測プログラム

ゲームを作り込めば作り込む程だんだんパフォーマンスが悪くなるものです。 そんなときは計算処理、描画処理のループ部分に計測用プログラムを最初から組み込んでおくと、思わぬボトルネックに気付く事もあります。 せっかくですので、現在個人的に開発中の次回作で使用している計測ユーティリティを載せておきます。 Androidであればコピペして使えるプログラムなので便利ですよ。
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Logger;

public class BenchTable {
    
    private static BenchTable me = new BenchTable();
    
    private HashMap ones = new HashMap();
    private HashMap sums = new HashMap();
    
    public static BenchTable getInstance(){
        return me;
    }
    
    public void start(String key){
        One o = ones.get(key);
        if(o == null){
            o = new One();
            ones.put(key, o);
        }
        o.start = System.currentTimeMillis();
    }
    
    public void stop(String key){
        One o = ones.get(key);
        if(o == null) return;
        
        o.end = System.currentTimeMillis();
        
        Sum s = sums.get(key);
        if(s == null){
            s = new Sum();
            sums.put(key, s);
        }
        s.set(o.end - o.start);
    }
    
    public void dump(){
        Logger logger = Logger.getLogger(this.getClass().getName());
        for (Iterator it = sums.entrySet().iterator(); it.hasNext();) {
            Map.Entry entry = (Map.Entry)it.next();
            String key   = (String)entry.getKey();
            Sum value = (Sum)entry.getValue();
            logger.info("Bench : " + key
                    + " avg :" + (value.total / value.count) 
                    + " max :" + value.max
                    + " total :" + value.total
                    + " count :" + value.count
                    );
            value.reset();
        }
    }
}

class One {
    double start;
    double end;
}

class Sum{
    int count;
    double total;
    double max;
    
    void set(double t){
        count++;
        total += t;
        if(max < t)max = t;
    }
    
    void reset(){
        count = 0;
        total = 0;
        max = 0;
    }
}
BenchTableクラスでは、計測したい箇所の呼び出し回数、合計時間、最大時間、平均時間を HashTableで管理します。 使い方は下記で、気になる関数の前後にラベルを指定してstartとstopを呼び出します。 一定期間が過ぎたらdumpを呼び出せば、集めた結果を出力します。
    private int bm = 0;

    public synchronized void onCalclateObjects(){
     if(state != GameEngineState.GAME_START)return;
     
     if(bm++ > 300){
      bm=0;
         BenchTable.getInstance().dump();
     }

     BenchTable.getInstance().start("phaseCalcAvatars");
     phaseCalcAvatars();
     BenchTable.getInstance().stop("phaseCalcAvatars");
     BenchTable.getInstance().start("phaseCalcFieldItem");
     phaseCalcFieldItem();
     BenchTable.getInstance().stop("phaseCalcFieldItem");
     BenchTable.getInstance().start("phaseCalcAtacks");
     phaseCalcAtacks();
     BenchTable.getInstance().stop("phaseCalcAtacks");
     BenchTable.getInstance().start("phaseCollisionAvatars");
     phaseCollisionAvatars();
     BenchTable.getInstance().stop("phaseCollisionAvatars");
    }
出力結果はこんな感じです。
02-09 11:32:12.272: INFO/BenchTable(27115): Bench : phaseCalcAtacks avg :0.019867549668874173 max :1.0 total :6.0 count :302
02-09 11:32:12.282: INFO/BenchTable(27115): Bench : phaseCollisionAvatars avg :0.8377483443708609 max :3.0 total :253.0 count :302
02-09 11:32:12.292: INFO/BenchTable(27115): Bench : phaseCalcAvatars avg :1.1986754966887416 max :27.0 total :362.0 count :302
02-09 11:32:12.292: INFO/BenchTable(27115): Bench : phaseCalcFieldItem avg :0.10264900662251655 max :1.0 total :31.0 count :302
count :302

最後に、この計測プログラムで自分のアプリを調べたときに残念な事がわかりました。 「AndroidはJNIが遅いので、OpenGLの限界に達する前にJNIの限界でポリゴン数が限られてしまう」 Javaを使ってAndroidアプリを作ったときに3D処理にOpenGLを使うと思うのですが、おおよそ下記のような呼び出しがされます。
Javaアプリ -> OpenGL関数呼び出し -> JNI -> ネイティブOpenGL -> GPU
最近のAndroid端末のGPUはそれなりの性能のものが搭載されていると思うので、それなりのポリゴンを処理できるはずなのですが、JNIがそれほど早くないので、JavaからネイティブOpenGLAPIを呼び出す回数自体が限られてしまうのです。 なのでJavaでOpenGLの処理を書いてしまうとGPUの性能をフルに使う事が出来ない事になります。 本格的な3Dのゲームを作りたかったらC++等で作らないとダメそうですね。残念 orz