-
Notifications
You must be signed in to change notification settings - Fork 0
ミニゲームの作り方
ミニゲームを作る方法をRaceGame(レースはしない)を例に簡単に説明します。
必要なファイルは次の通りです。
- symbols (オブジェクトをしまう場所)
- Car.java
- Background.java
- RaceGameController.java (コントローラ)
- RaceGameModel.java (モデル)
- RaceGameScene.java (シーン)
- RaceGameView.java (ビュー)
これらを組み合わせることでゲームを作ることができます。
ここではゲームに使うオブジェクトを管理します。このゲームでは車オブジェクトと背景オブジェクトを作っています。
public class RaceGameModel extends GameModel {
private Car car;
private Background background;
public RaceGameModel() {
car = new Car();
background = new Background();
}
public Car getCar() { return car; }
public Background getBackground() { return background; }
// 略
}
こんな感じで書ければ大丈夫です。
車オブジェクトを作ってみましょう。このゲーム全体ではオブジェクトのことを Symbol
と言います。(別に深い意味はないが……)
動くシンボルなので extends MovableSymbol
したクラス Car
を作ります。
public class Car extends MovableSymbol {
private Image image;
final private int MAX_SPEED = 500;
private int nowspeed = 0;
// 速度調整用タイマー
private final Timer speedTimer = new Timer(10, e -> {
if(nowspeed > 0) accelerate(-8);
if(nowspeed < 0) accelerate(8);
setSpeedPxPerSecond(nowspeed); // MovableSymbolのAPI
moveMilliseconds(10); // MovableSymbolのAPI
});
public Car() {
// 初期位置の設定 (SymbolのAPI)
setLocation(100, 100);
// 速度の設定 (MovableSymbolのAPI)
setSpeedPxPerSecond(nowspeed);
// 当たり判定の設定
var hitbox = RelativeHitBox.makeRectangle(80, 40);
this.setRelativeHitBox(hitbox);
}
/**
* 加速します
* @param a 加速度
*/
public void accelerate(int a) {
if(a > 0) {
nowspeed = Math.min( MAX_SPEED, nowspeed + a);
} else {
nowspeed = Math.max(-MAX_SPEED, nowspeed + a);
}
}
}
ここでオブジェクトに関する設定を色々すれば良いです。Symbolの関係のAPIについてはここを読めば良いです。
タイマーを使ったので、終了時用に停止処理を書く必要があります。また、再開時用に再開処理も書くと良いでしょう。 Symbol
と MovableSymbol
にはこれをサポートするメソッドがないので、Suspendable
を実装します。
public class Car extends MovableSymbol implements Suspendable {
// 略
@Override
public void suspend() {
speedTimer.stop();
}
@Override
public void resume() {
speedTimer.start();
}
}
これで再開/停止処理ができました。次にSymbolなので描画ができないと困ります。描画にはSymbolのAPIの setDrawer(Drawable)
を使います。Drawableは描画用のインタフェースです。
public interface Drawable {
void draw(Graphics2D g);
}
ラムダ式を使って setDrawer(g -> g.drawImage(...))
としても良いのですが、ここではDrawableを実装して setDrawer(this)
する方法を考えます。
public class Car extends MovableSymbol implements Drawable, Suspendable {
private Image image;
// 略
public Car() {
// 略
// 画像の設定
Path imagePath = Paths.get(".", "resource", "mini_game", "race_game", "car.png");
image = Toolkit.getDefaultToolkit().getImage(String.valueOf(imagePath));
// Drawableを乗せる
setDrawer(this);
}
@Override
public void draw(Graphics2D g) {
Rectangle rect = new Rectangle((int)getX(), (int)getY(), 0, 0);
rect.grow(40, 20);
// 回転して画像をdraw
double angle = this.getAngleRadians();
g.rotate(angle, (int)getX(), (int)getY());
g.drawImage(image, (int)rect.getX(), (int)rect.getY(),
(int)rect.getWidth(), (int)rect.getHeight(), null);
// 回転を戻さないと後の描画が狂う
g.rotate(-angle, (int)getX(), (int)getY());
}
}
画像は resource/mini_game/race_game
配下に置きます。また、取得は Paths.get
して得たPathを Toolkit#getImage
で取得してください。Paths.getしないと、ファイルパスが違うOSでファイルを読み込めなくなってしまいます。
今回は行っていませんが、画面サイズの取得は MainView.getInstance().getWidth()
と MainView.getInstance().getHeight()
で行うことができます。
このゲームでは当たり判定が実装されていませんが、ここを読むと理解できるでしょう。(理解できるのか?)
理解できないと思うのでざっくりまとめると
- Symbolの座標を中心の長方形状の当たり判定を作りたい!
RelativeHitBox.makeRectangle(int 幅, int 高さ)
- Symbolの座標を中心の円形の当たり判定を作りたい!
RelativeHitBox.makeRectangle(int 半径)
- 凝った形の当たり判定を作りたい!
new RelativeHitBox(Shape 形状)
を使って RelativeHitBox
インスタンスを作り、Symbolに登録 (Symbol#setRelativeHitBox(RelativeHitBox)
) すると当たり判定の枠を作ることができます。
判定には Symbol#touches(Symbol)
を使います。touches
の実装は次の通りです。
public boolean touches(Symbol other) {
Area myHitBox = this .getRelativeHitBox().createAbsoluteHitBoxArea(this);
Area otherHitBox = other.getRelativeHitBox().createAbsoluteHitBoxArea(other);
myHitBox.intersect(otherHitBox);
return !myHitBox.isEmpty();
}
このように相対的な位置で記録する RelativeHitBox
に Symbol
を渡して、絶対的な位置の図形オブジェクトを取得、それが交差しているかどうかで判定しています。ですから、完全に包含しているなどの細かい当たり判定の条件を作る場合はこのように RelativeHitBox#createAbsoluteHitBoxArea
を使えば良いです。
あくまで RelativeHitBox
は相対的な位置で記録するだけで、これ自体に判定するメソッドが用意されていないことに気をつけてください。
Car
で Suspendable
を実装しましたが、これでは不十分です。モデルから呼び出す必要があります。モデルの最後に次のコードを付け加えましょう。
@Override
public void suspend() {
car.suspend();
}
@Override
public void resume() {
car.resume();
}
次にViewを作ります。Viewなのでまず GameView
を継承します。あとはコードの通りです。
public class RaceGameView extends GameView {
private RaceGameModel model;
private Timer paintTimer = new Timer(10, g -> repaint());
public RaceGameView(RaceGameModel model) {
super(model);
this.model = model;
setBackground(new Color(0x111111));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// 背景のトラックを描画
model.getBackground().getDrawer().draw((Graphics2D) g);
// 車を描画
model.getCar().getDrawer().draw((Graphics2D) g);
}
// paint用のタイマーを制御
@Override
public void suspend() { paintTimer.stop(); }
@Override
public void resume() { paintTimer.start(); }
}
ViewはModelから情報をもらって画面に出力するくらいが書ければOKです。paint用のタイマーをstop/startするのを忘れないようにしましょう。
public class RaceGameController extends GameController implements KeyListener {
private RaceGameModel model;
private RaceGameView view;
private boolean active = false;
public RaceGameController(RaceGameModel model, RaceGameView view) {
super(model, view);
this.model = model;
this.view = view;
// view に listener を add する
view.addKeyListener(this);
}
// 操作用タイマー
private final Timer forwardTimer = new Timer(10, e -> model.getCar().accelerate(10));
@Override
public void keyTyped(KeyEvent e) {}
@Override
public void keyPressed(KeyEvent e) {
if(!active) return;
switch(e.getKeyChar()) {
case 'w' -> forwardTimer.start();
}
}
@Override
public void keyReleased(KeyEvent e) {
if(!active) return;
switch(e.getKeyChar()) {
case 'w' -> forwardTimer.stop();
}
}
// この2つでタイマーの制御をする
@Override
public void suspend() {
active = false;
forwardTimer.stop();
}
@Override
public void resume() { active = true; }
}
こうです。(雑) 無駄にコードが長いだけでしてることは大したことないです。KeyListener
とか MouseListener
とかを実装して、Viewに貼り付けてあげます。
押している間動かし続ける処理にはタイマーを使うことに気をつけてください。keyPressed()にそのまま書くとうまく動きません。詳細はここを読んでください。
授業でやったMVCパターンと決定的に異なるのがこの「Scene」です。なぜこれが用意されているのかというと「ミニゲームが集まったゲームだからゲームのまとまったオブジェクトが欲しいよね!」という理由です。理由からわかる通り、Sceneでは、model, view, controllerを全て管理します。それだけです。 !追記!makeDescriptionDialog()をしないと画面が出てきません
public class RaceGameScene extends MiniGameScene {
private RaceGameModel model;
private RaceGameView view;
public RaceGameScene() {
model = new RaceGameModel();
view = new RaceGameView(model);
setModel(model);
setView(view);
setController(new RaceGameController(model, view));
setTitle("Sample Race Game");
setCreatorName("TrpFrog");
setDescription("ようこそつまみ自動車学校へ!");
setHowToPlay("まずキーボードを使います");
makeDescriptionDialog();
}
// テスト用にこういうのがあると便利
public static void main(String[] args) {
SceneManager.getInstance().push(new RaceGameScene());
}
}
本当にこれ以外書きません。mainメソッドがうまく動けば完成です!
net.trpfrog.medipro_game.pause.EscapeToPause
をnewして addKeyListener
するとESCキーでポーズできるようになります。簡単!
1つしか関数がないインタフェースを実装したクラスを即席で作るための文法です。例えばTimerクラスは引数に ActionListener
を要求してくるので
class LambdaExample implements ActionListener {
private Timer timer;
public LambdaExample() {
timer = new Timer(10, this);
timer.start();
}
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("fired");
}
}
する必要がありますが、ラムダ式を使うと
class LambdaExample {
private Timer timer;
public LambdaExample() {
timer = new Timer(10, e -> System.out.println("fired!"));
timer.start();
}
}
と書くことができます。もちろんこのゲーム用につまみさんが作ったインタフェースの Drawable
に対しても使うことができます。
class LambdaExample extends Symbol {
public LambdaExample() {
this.setDrawer(g -> {
g.setColor(new Color(0x90e200));
g.fillOval(0, 0, 100, 100);
g.setColor(Color.ORANGE);
g.fillRect(0, 50, 100, 75);
}
}
}
2行以上の場合は波かっこを使えば良いです。詳しくは調べてみてください。
net.trpfrog.medipro-game.util.GifConverter
を使うとGIFアニメを List<Image>
に変換することができます。
これで出来上がったリストを ImageAnimationSymbol
に渡して、setFps(int)
して、 start()
するとアニメートされます。もちろん paintComponent
から draw(Graphics2D g)
を呼び出してあげる必要があります。詳しくはExplosionAnimation.javaかImageAnimationSymbolのJavadocを読むと良いです。
こうして
public void draw(Graphics2D g) {
float alpha = 0.5f
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
g.drawImage(...);
g.drawImage(...);
// 終わったら戻す
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
}