【JavaFX:横スクロールアクションゲーム】プレイヤーキャラ、敵キャラの表示と動きを実装
キャラクタークラスを作成し、プレイヤーキャラを表示させます。
前の記事 | 横スクロールアクションゲームTop | 次の記事 |
CharaObject
CharaObjectクラスはすべてのキャラクターオブジェクトが継承するクラスです。このゲームでは、プレイヤーキャラと敵キャラに分岐させます。
汎用性を持たせるなら、プレイヤーという操作込みのクラスには分けないほうが良い気がしますが、今回は一緒こたにしちゃいます。
CharaObjectクラスはupdateメソッドを実装し、必ず処理させたい内容を記述します。実装しておきたい処理は下記。
・移動前の座標を保持する処理
・無敵状態の処理
・抽象メソッド、moveの呼び出し
まずは移動前座標を保持する理由ですが、ブロック判定でより簡単に判定処理を実装させることができます。移動前座標を保持して判定を行う方法は他のゲームを制作する時も役にたつと思います。
次に無敵状態の処理ですが、この無敵状態がないと、フレームごとにキャラクターがダメージを受けてしまいます。その他にも前回ダメージからの時間を計算して実装する方法もありますが、無敵時間を指定するほうが簡単そうなのでこっちを採用。
抽象メソッド、moveはオーバーライド先で個々の動きを実装していきます。Playerならマウス右クリックでジャンプするとかかな。updateメソッド内で呼び出すようにしておきます。
これ以外にもキャラクターが今地上にいるか、空中にいるかなどのフラグも保持します。
キャラクターの落下処理はサブクラスで実装予定ですが、アクションゲームでは下へ落下する動きがデフォルトの場合が多くなるので、この処理もCharaObjectに実装したほうが楽かもしれませんね。
//キャラの無敵時間 int invisibleCount; double prel; double pret; double prer; double preb; //空中にいるかどうか protected boolean isAir; //地上にいるかどうか protected boolean isGround; public CharaObject(Node viewNode, Node hitNode) { super(viewNode, hitNode); positionInit(); } @Override public void update() { if ( isInvisible() ) { invisibleCount--; getViewNode().setOpacity(0.5); } else { getViewNode().setOpacity(1); } //移動前の座標を保持しておく positionInit(); move(); } //現在の座標を記録しておく protected void positionInit() { Bounds b = getHitNode().getBoundsInParent(); prel = b.getMinX(); pret = b.getMinY(); prer = b.getMaxX(); preb = b.getMaxY(); } //無敵状態かを返す public boolean isInvisible() { return invisibleCount > 0; } //キャラがダメージを負った時に呼び出すメソッド public void onDamage() {} //移動処理を行う抽象メソッド public abstract void move();
Player
Playerクラスはプレイヤーキャラを表すクラスで、マウス操作を検出し、移動処理やジャンプを実装します。
double fallSpeed = 0; int speed = 3; boolean isHighJump = false; public Player(Node viewNode, Node hitNode) { super(viewNode, hitNode); ((ImageAnimationView)viewNode).setAnimationRange(1, 4); } @Override public void move() { boolean isRight = PlayerControllerManager.isRight(); if ( isGround ) { //地上だったら落下速度0 fallSpeed = 0; if ( isHighJump || PlayerControllerManager.isJump()) { //ジャンプしたなら落下速度を逆にする fallSpeed = isHighJump ? -20 : -15; AppManager.playSE("se4"); isHighJump = false; } } else if ( isAir ) { if ( fallSpeed < 15 ) { //空中だったら落下速度アップ fallSpeed++; } } //移動処理と画像反転処理 ImageAnimationView img = (ImageAnimationView) getViewNode(); if ( isRight ) { //右へ移動 moveX(speed); img.setScaleX(1); } //画像の切り替え処理 if ( isAir ) { if ( img.getStatus() == Animation.Status.RUNNING ) { img.stop(); } img.setIndex(img.getMaxIndex()); } else if ( isRight ) { if ( img.getStatus() != Animation.Status.RUNNING ) { img.setIndex(1); img.play(); } } else { if ( img.getStatus() == Animation.Status.RUNNING ) { img.stop(); } img.setIndex(0); } //上下移動処理 moveY(fallSpeed); } public void onDamage() { invisibleCount = 120; if ( speed > 5 ) { speed-=3; } else if ( speed > 4 ) { speed-=2; } else if ( speed > 1 ) { speed--; } } @Override public void mouseAction() { //クリックされたらハイジャンプ if ( isGround ) { isHighJump = true; } }
PlayerControllerManager
マウス操作はPlayerControllerManagerクラスにて実装します。このクラスは単純にマウス操作を検出し、プレイヤーの操作内容を取得できるだけのクラスです。
Playerクラスから、このクラスへアクセスし、移動処理を実装していきます。
//マウスマネージャー static MouseManagerFX mMng = GTKManagerFX.getMouseManager(); //このフラグがオフの場合は常にキー入力を無効化する static boolean isActive=true; static { //検出マウスボタンの登録 mMng.regist(MouseButton.PRIMARY); mMng.regist(MouseButton.SECONDARY); } //このフラグがオフの場合は常にアクションフラグを無効化する public static void setActive(boolean isActive) { PlayerControllerManager.isActive =isActive; } //ジャンプすべきか() public static boolean isJump() { return isActive && mMng.isPress(MouseButton.SECONDARY); } //ジャンプボタンを押下し続けているか public static boolean isJumping() { return mMng.isDown(MouseButton.SECONDARY); } //右移動すべきか public static boolean isRight() { return isActive; } //マウスの左クリック public static boolean isMPressLeft() { return isActive && mMng.isPress(MouseButton.PRIMARY); } //マウスのX座標を取得します public static double getMX() { return mMng.getPoint().x; } //マウスのY座標を取得します public static double getMY() { return mMng.getPoint().y; }
Enemy
敵キャラを表すスーパークラスです。
敵キャラはデフォルトでは受けると倒せるようにしておきます。onDamageメソッドはキャラがダメージを受けたときに呼び出されます。
@Override public void onDamage() { //ダメージ受けたら倒される isAlive = false; }
各種敵キャラはこのクラスを継承して個々の処理を記述します。
今回は敵キャラは2体のみ
・スライム
Slimeクラスとして定義し、右側へ地上を移動するだけの単純処理
・バード
Birdクラスとして定義し、右側へ空中移動するだけの単純処理
例えばスライムクラスのmoveメソッドはこんな感じにしました。
@Override public void move() { if ( isGround ) { //地上だったら落下速度0 fallSpeed = 0; } else if ( isAir ) { //空中だったら落下速度アップ fallSpeed++; } //上下移動処理 moveY(fallSpeed); //左右移動 moveX(speed); }
ステージの表示とキャラクターの実装サンプル
最後にプレイヤーと敵キャラを表示させる例を紹介しておきましょう。
import static com.nompor.gtk.fx.GTKManagerFX.*; import com.nompor.gtk.fx.GTKManagerFX; import com.nompor.gtk.fx.GameViewFX; import com.nompor.gtk.fx.animation.ImageAnimationView; import com.nompor.gtk.fx.image.ImageManagerFX; import javafx.animation.Animation; import javafx.application.Application; import javafx.scene.image.ImageView; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import javafx.util.Duration; public class Test2 extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws Exception { final int WIDTH = 800, HEIGHT = 600; GTKManagerFX.start(primaryStage,WIDTH, HEIGHT); //GameViewFXはGroupを継承したクラスで、processメソッドはゲームループの処理を実装する GTKManagerFX.changeView(new GameViewFX() { ImageManagerFX imgMng = GTKManagerFX.getImageManager(); Player p; Enemy e; GameField fields; @Override public void start() { //ウィンドウ表示領域800*600に合わせて二次元配列構築 final int ROW=HEIGHT/50,COL=WIDTH/50; Field[][] field = new Field[ROW][COL]; //画面一番下に地面ブロック配置 final int UNDER_INDEX = (ROW-1); for ( int i = 0;i < COL;i++ ) { Rectangle rect = new Rectangle(i * 50,UNDER_INDEX*50,50,50);//当たり判定Node ImageView imgView = new ImageView(imgMng.getImage("img/jimen.png"));//表示Node imgView.setTranslateX(rect.getX()); imgView.setTranslateY(rect.getY()); field[UNDER_INDEX][i] = new Block(imgView, rect); } //画面一番下から二つ上にいくつかブロック配置 final int BLOCK_INDEX = (ROW-3); for ( int i = 5;i < 11;i++ ) { Rectangle rect = new Rectangle(i * 50,BLOCK_INDEX*50,50,50);//当たり判定Node ImageView imgView = new ImageView(imgMng.getImage("img/block.png"));//表示Node imgView.setTranslateX(rect.getX()); imgView.setTranslateY(rect.getY()); field[BLOCK_INDEX][i] = new Block(imgView, rect); } //装飾を追加 ImageView imgView = new ImageView(imgMng.getImage("img/kusa.png"));//表示Node final int idxRow = UNDER_INDEX-1,idxCol = 3; imgView.setTranslateX(idxCol*50); imgView.setTranslateY(idxRow*50); field[UNDER_INDEX-1][3] = new Field(imgView); //GameFieldオブジェクトの構築 fields = new GameField(field); Field[][] fieldList = fields.getFieldList(); for ( int i = 0;i < fieldList.length;i++ ) { for ( int j = 0;j < fieldList[i].length;j++ ) { Field fld = fieldList[i][j]; if ( fld != null ) { getChildren().add(fld.getViewNode()); } } } //プレイヤーの作成 ImageAnimationView view = createImageAnimationView(Duration.millis(500), imgMng.getImage("img/mogmol.png"), 50, 50);//表示ノード view.setCycleCount(Animation.INDEFINITE); view.setIndex(0); Rectangle rect = new Rectangle(10,510,30,37);//当たり判定 view.setTranslateY(500); p = new Player(view, rect); view.play(); //プレイヤーの表示 getChildren().add(p.getViewNode()); //敵の作成 ImageAnimationView img = createImageAnimationView(Duration.millis(300), imgMng.getImage("img/slime.png"),50,50);//表示ノード img.setCycleCount(Animation.INDEFINITE); img.setIndex(0); Rectangle r = new Rectangle(300+8,550+26,35,20);//当たり判定 img.setTranslateX(300); img.setTranslateY(550); e = new Slime(img, r); //敵表示 getChildren().add(e.getViewNode()); } @Override public void process() { //ゲームループ move(p); move(e); } private void move(CharaObject o) { o.moveX(1.5); o.moveY(7); //ウィンドウ表示領域より下に行ったら止まるようにする double y = o.getHitNode().getBoundsInParent().getMaxY(); if(y>=HEIGHT) { o.moveY(-(y-HEIGHT)); o.isGround = true; } else { o.isGround = false; } o.isAir = !o.isGround; } }); } }
本稿の全ソースはこちらです。
ディスカッション
コメント一覧
まだ、コメントがありません