【Java:的当てゲーム】的を破壊する機能の実装

2018年8月16日

前回の記事で的を出現させるところまで実装しましたので、今回は的を破壊する部分を実装していきたいと思います。

前の記事 的当てゲーム制作Top 次の記事

マウスの左ボタンを押したときに的を消す

マウスの左ボタンを押した時に的を消す処理を実装します。

Targetクラスに当たり判定をするメソッドを実装します。

Target.java
	//的との当たり判定用メソッド
	public boolean isHit(Point p) {
		return circle.contains(p.x, p.y);
	}

次にGameMode.java側の実装ですが、反応速度が要求されるため、マウスのボタンを押した瞬間に反応するように、mousePressedメソッドをオーバーライドして実装します。そして、TargetクラスのisHitメソッドがtrueなら的をリストから削除します。その際、破壊効果音を再生するようにしておきましょうか。

GameMode.java
	//マウスボタンを押し下げたときの処理
	//反応速度が要求されるためmousePressedで処理する
	public void mousePressed(MouseEvent e) {

		//当たり判定
		int hit = 0;
		switch ( e.getButton() ) {
			case MouseEvent.BUTTON1:
				for ( int i = viewTargetList.size() - 1;i >= 0;i-- ) {
					Target t = viewTargetList.get(i);

					//ターゲットが存在していてマウス座標があっていたら破壊アニメーションを実行
					if ( t.isHit(e.getPoint()) ) {
						viewTargetList.remove(i);
						hit++;
					}
				}
				if ( hit > 0 ) {
					AppManager.breakSE();
				}
				break;
		}
	}
実行結果

ちゃんと消える処理が実装できていることがわかります。

得点加算と得点表示アニメーションの実装

的を消した時に得点加算の処理と、何点の得点を付与したかわかるように、的が消えた瞬間に中心から数値を上にアニメーションさせます。

得点の計算処理はマウス座標が中心点までどのくらいの割合の位置に存在するか求めて、その割合×100で計算するものを実装することにしました。最初は中心点に近づくにつれて、10,30,50,100みたいな感じを想定していたのですが、こっちのほうがプレイするたびに得点差が出やすく、やりこみ要素のある内容になりそうだと判断し、この計算方法を採用することとしました。

計算処理はこんな感じで実装してみました。もっと良い方法があるかもしれません。回りくどいやり方していたらごめんなさい。

	//的と中心点との計算を実行しスコアポイントを計算
	public int getScorePoint(Point p) {
		//中心点への距離が近いほど高得点を返すように処理する
		double r = circle.getRadius();
		r *= r;

		double vx = p.x - circle.cx;
		double vy = p.y - circle.cy;

		double r2 = vx*vx+vy*vy;
		int result = (int)Math.round((1.0 - r2 / r) * 100);

		lastScorePoint = (result < 0 ? 0 : result);
		return lastScorePoint;
	}

次に得点表示アニメーションは的の破壊後に一定フレーム数経過するまで数値の文字列を描画するような処理を実装します。その際y座標を減算させて上昇するアニメーションとなるように実装してみます。

まずはTargetクラスに得点の計算を行うgetPointメソッドを実装し、得点が取得されたら、その点数を文字列で描画します。


	private int lastScorePoint=-1;
	private int deleteFrame=0;
	private int sy;

	//描画
	public void draw(Graphics g) {

		if ( !isAlive ) {

			deleteFrame++;
		} else {
			circle.cx+=mx;
			circle.cy+=my;
			g.drawImage(AppManager.getTargetImage()
					, circle.getICX()-50, circle.getICY()-50, circle.getICX()+50, circle.getICY()+50
					, 0, 0, 100, 100
					, null);

		}

		if ( lastScorePoint != -1 ) {
			//破壊後の得点の描画
			sy--;
			g.setColor(Color.WHITE);
			g.setFont(font);
			GraphicsUtil.drawCenteringString(g, ""+lastScorePoint, circle.getICX(), circle.getICY()+sy);
		}
	}

なんか汚らしい実装な気もします。lastScorePointは得点計算後に値が設定されますので、それで判定しています。

点数文字列はTargetクラスが描画するような実装にしましたので、新たにisDeleteメソッドを作成し、これがtrueを返すまでは文字列アニメーションを表示するようにします。isAliveメソッドは見た目上破壊されているかどうかを返すようにします。breakProcessメソッドで的の破壊要求を出すメソッドも作成します。後でGameMode.javaの削除タイミングも修正が必要になります。

	//的が破壊されていないか
	public boolean isAlive() {
		return isAlive;
	}

	//的を削除するか
	public boolean isDelete() {
		return !isAlive && deleteFrame > 60;
	}

	//破壊処理の要求メソッド
	public void breakProcess() {
		isAlive = false;
	}

GameMode.javaのほうはgetScorePointメソッドを呼び出し、その得点分スコアに加算します。ついでにコンボ数や破壊数も加算しておき、コンボ数に関しては、的にヒットしなかった場合に0となるように修正しておきました。そしてコンボ数と取得得点で乗算してトータルスコアに加算していくことにします。Targetクラスの削除タイミングを変えないといけないので、これも忘れずに修正しておきます。

マウスイベントの処理を変更

	//マウスボタンを押し下げたときの処理
	//反応速度が要求されるためmousePressedで処理する
	public void mousePressed(MouseEvent e) {

		//当たり判定
		int hit = 0;
		switch ( e.getButton() ) {
			case MouseEvent.BUTTON1:
				//ブロックに当たっていたら的の当たり判定はしない
				for ( int i = viewTargetList.size() - 1;i >= 0;i-- ) {
					Target t = viewTargetList.get(i);

					//ターゲットが存在していてマウス座標があっていたら破壊アニメーションを実行
					if ( t.isAlive() && t.isHit(e.getPoint()) ) {
						//マウス座標を引き渡し、得点を取得
						int point = t.getScorePoint(e.getPoint());
						combo++;
						score+=point*combo;
						breakNum++;
						maxCombo = Math.max(combo, maxCombo);

						//Targetへ破壊を通知
						t.breakProcess();
						hit++;
					}
				}
				if ( hit > 0 ) {
					AppManager.breakSE();
				} else {
					combo = 0;
				}
				break;
		}
	}

描画処理の修正

			//ゲーム中の描画処理
			for ( int i = viewTargetList.size() - 1;i >= 0;i-- ) {
				Target t = viewTargetList.get(i);

				//Targetオブジェクトの削除フラグがtrueか画面外に出たら削除
				if ( t.isDelete()
					|| t.getRight() < -50
					|| t.getBottom() < -50
					|| t.getLeft() > width+50
					|| t.getTop() > height+50) {

					viewTargetList.remove(i);
				} else {
					//Targetの移動処理と描画
					t.draw(g);
				}
			}
実行結果

いい感じですね。

もうこの時点でちょっとしたゲームとしては遊べそうですね。

破壊アニメーションの実装

破壊アニメーションを実装します。複数枚画像を利用したアニメーションはライブラリのImageDrawAnimationクラスで実装出来るようにしています。このクラスの引数には、結合済みの複数画像を渡し、後続の引数に画像1枚分の大きさを指定すると、そのサイズで切り抜き描画してくれるクラスで、updateメソッドを呼び出すと1フレーム進行します。

このクラスをうまく使用して破壊アニメーションを実装します。

おまけのアニメーションとして、拡大、フェードアウト、回転を破壊時に同時適用させ、微妙にいい感じの破壊エフェクトになるように細工します。使用するアニメーションクラスはParallelAnimation、ScaleAnimation、FadeAnimation、RotateAnimationです。使用方法はカウントダウンの稿で説明した内容と変わりません。

それでは最終的なTargetクラスの実装を見ておきましょうか。


import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;

import com.nompor.gtk.animation.FadeAnimation;
import com.nompor.gtk.animation.ImageDrawAnimation;
import com.nompor.gtk.animation.ParallelAnimation;
import com.nompor.gtk.animation.RotateAnimation;
import com.nompor.gtk.animation.ScaleAnimation;
import com.nompor.gtk.draw.GraphicsUtil;
import com.nompor.gtk.geom.Circle;
//的を表すクラス
public class Target{
	private Circle circle;
	private ImageDrawAnimation img;
	private int mx;
	private int my;
	private boolean isAlive=true;
	private int lastScorePoint=-1;
	private int sy;
	private int deleteFrame;
	private static Font font = new Font(Font.MONOSPACED, Font.BOLD, 20);

	//並列アニメーションオブジェクト
	//拡大、フェードアウト、回転を同時実行する
	private ParallelAnimation animation;
	public Target(int x, int y, int mx, int my) {
		//画像アニメーション
		this.img = new ImageDrawAnimation(AppManager.getTargetImage(),100,100,1);
		animation = new ParallelAnimation(
				new ScaleAnimation(img, 0.1)
				, new FadeAnimation(img, -0.12f)
				, new RotateAnimation(img, 10)
		);

		//最後の画像を表示したら終了
		img.setAnimationEndOfLastImageIndex();

		//円オブジェクト(引数x,yは画像の左上座標を想定しているため+50が的の中央
		circle = new Circle(x+50, y+50, 25);

		//移動量
		this.mx = mx;
		this.my = my;
	}

	//的が破壊されていないか
	public boolean isAlive() {
		return isAlive;
	}

	//的を削除するか
	public boolean isDelete() {
		return !isAlive && deleteFrame > 60;
	}

	//破壊処理の要求メソッド
	public void breakProcess() {
		isAlive = false;
	}

	//描画
	public void draw(Graphics g) {


		//破壊されたら破壊アニメーション
		if ( !isAlive ) {
			//アフィン変換アニメーション
			animation.update();

			//画像差し替えアニメーション
			img.update();

			deleteFrame++;
		} else {
			circle.cx+=mx;
			circle.cy+=my;
		}
		//アニメーション結果を適用した描画を実行
		img.draw(g, circle.getICX()-50, circle.getICY()-50);

		if ( lastScorePoint != -1 ) {
			//破壊後の得点の描画
			sy--;
			g.setColor(Color.WHITE);
			g.setFont(font);
			GraphicsUtil.drawCenteringString(g, ""+lastScorePoint, circle.getICX(), circle.getICY()+sy);
		}
	}

	//的との当たり判定用メソッド
	public boolean isHit(Point p, Circle c) {
		return c.contains(p.x, p.y);
	}
	//的との当たり判定用メソッド
	public boolean isHit(Point p) {
		return isHit(p, circle);
	}
	//的と中心点との計算を実行しスコアポイントを計算
	public int getScorePoint(Point p) {
		//中心点への距離が近いほど高得点を返すように処理する
		double r = circle.getRadius();
		r *= r;

		double vx = p.x - circle.cx;
		double vy = p.y - circle.cy;

		double r2 = vx*vx+vy*vy;
		int result = (int)Math.round((1.0 - r2 / r) * 100);

		lastScorePoint = (result < 0 ? 0 : result);
		return lastScorePoint;
	}
	//的の左端座標を返す
	public int getLeft() {
		return circle.getILeft();
	}
	//的の上端座標を返す
	public int getTop() {
		return circle.getITop();
	}
	//的の右端座標を返す
	public int getRight() {
		return circle.getIRight();
	}
	//的の下端座標を返す
	public int getBottom() {
		return circle.getIBottom();
	}
}
実行結果

今回の実装でTarget.javaの実装は完了となりますので、いつも通りgithubにアップしておきます。

Java

Posted by nompor