Murayama blog.

AIの民主化。

角度のついた跳ね返り。


跳ね返りについては、第6章で一度習ってるんだけど、
第10章ではそれを発展させて、角度のついた跳ね返りを習います。
どういうプログラムかというと、

やっぱり微妙にサイズがあってないけど気にしない。


まずは、跳ね返りの復習から。

	if(ball.x -ball.radious < 0){
		ball.x = ball.radious;
		ball.vx *= bounce;
	}else if(ball.x + ball.radious > stage.stageWidth){
		ball.x = stage.stageWidth - ball.radious;
		ball.vx *= bounce;
	}


このコードは、ボールが画面内を移動していたとして、
画面の横幅を超えてしまった場合、つまり衝突した場合に跳ね返るようにしています。
速度×-1とすることで、逆方向への速度を得ることができます。
実際は跳ね返る度に減速していくことが多いので、×-0.6とかで少しずつ速度を弱めることが多いです。


あと、衝突した瞬間は移動しているオブジェクト(この場合ball)が境界を超えてしまう(めりこんでしまう)ので、
衝突直前の正しい位置に戻しているのがポイントです。


#説明の順番が逆の方がよかったかも。。


で、本題の角度のついた跳ね返りです。

これをどのようにプログラムで表現するか。
Keith(著者)は、めちゃくちゃハマったらしいです。
いろいろ試行錯誤をして悩んだあげく、Stuart氏からヒントをもらったそうです。
そんなKeithを救った言葉がこれだ。

"角度のついた跳ね返りかい?面が水平になるようにシステム全体を回転させるのさ、跳ね返らせたらそれを元に戻すんだ。"


つまり、さっきのイメージをこんなふうに考えればよい、と。


ここで、必要になるのが1つ前で習った公式です。座標回転の。

var x2:Number = cos * x1 - sin * y1;
var y2:Number = cos * y1 + sin * x1;


この公式を組み合わせれば角度のついた跳ね返りを実現できます。
プログラムの主要な部分を見てみます。
以下は、ENTER_FRAMEイベントで実行するコードの一部です。

	// ラインの角度から座標回転に必要なcosとsinを設定	
	var angle:Number = line.rotation * Math.PI / 180;
	var cos:Number = Math.cos(angle);
	var sin:Number = Math.sin(angle);
	
	// ボールとラインの距離
	var x1:Number = ball.x - line.x;
	var y1:Number = ball.y - line.y;				
	
	// 座標を回転することで、垂直な跳ね返りとして計算可能になる。
	var x2:Number = cos * x1 + sin * y1;
	var y2:Number = cos * y1 - sin * x1
	
	// 同様に、速度も回転させる
	var vx1:Number = cos * ball.vx + sin * ball.vy;
	var vy1:Number = cos * ball.vy - sin * ball.vx;
	
	// 距離がボールの半径より小さくなった(=衝突)かどうか
	if(y2 > -ball.height / 2 && y2 < vy1){
		
		// ボールをライン上に設定して、垂直成分の速度を反転
		y2 = -ball.height / 2;
		vy1 *= bounce;
		
		// 座標回転元に戻す
		x1 = cos * x2 - sin * y2;
		y1 = cos * y2 + sin * x2;
		// 同様に速度も元に戻す
		ball.vx = cos * vx1 - sin * vy1;
		ball.vy = cos * vy1 + sin * vx1;
		// 移動
		ball.x = line.x + x1;
		ball.y = line.y + y1;
	}

ちょっと長いけど、if文の手前まででやってるのは、
1つ前のサンプルとほとんど同じで、座標を回転させることで、ラインの角度を一旦無効にしているイメージです。
細かいことだけど、公式の+とーが反転しています。

	// 座標を回転することで、垂直な跳ね返りとして計算可能になる。
	var x2:Number = cos * x1 + sin * y1;
	var y2:Number = cos * y1 - sin * x1
	
	// 同様に、速度も回転させる
	var vx1:Number = cos * ball.vx + sin * ball.vy;
	var vy1:Number = cos * ball.vy - sin * ball.vx;

こんなふうに+ーを入れ替えることで、回転を逆に進めることができます。結果的にラインが水平になります。


真ん中のif文では、ボールとラインの衝突をチェックしています。
if文の中に入るということは、衝突した、ということなので、跳ね返り処理を実装しています。
跳ね返り処理の後半では、再び座標回転の公式を使って、無効にしていたラインの角度をもとに戻しています。


コード全文はこちら。

package
{
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.geom.Rectangle;
	
	public class MultiAngleBounce extends Sprite
	{
		
		private var lines:Array;
		private var lineCount:Number = 5;
		private var ball:Ball;
		private var gravity:Number = 0.3;
		private var bounce:Number = -0.6;
		
		private var isStart:Boolean = false;
		
		public function MultiAngleBounce()
		{
			init();
		}
		
		private function init():void{
			// ボールとライン5本の設定
			ball = new Ball(20);
			ball.x = 100;
			ball.y = 50;
			addChild(ball);
			
			lines = new Array;
			for(var i:uint = 0; i < lineCount; i++){
				var line:Sprite = new Sprite;
				line.graphics.lineStyle(1);
				line.graphics.moveTo(-50, 0);
				line.graphics.lineTo(50, 0);
				addChild(line);
				lines[i] = line;
			}
			
			lines[0].x = 100;
			lines[0].y = 100;
			lines[0].rotation = 30;			

			lines[1].x = 100;
			lines[1].y = 230;
			lines[1].rotation = 45;			

			lines[2].x = 250;
			lines[2].y = 180;
			lines[2].rotation = -30;			

			lines[3].x = 150;
			lines[3].y = 330;
			lines[3].rotation = 10;

			lines[4].x = 230;
			lines[4].y = 250;
			lines[4].rotation = -30;			
			
			stage.addEventListener(MouseEvent.CLICK, onClick_stage);
			addEventListener(Event.ENTER_FRAME, onEnterFrame);
		}

		private function onEnterFrame(evt:Event):void
		{

			if(!isStart){
				return;
			}
			
			// 重力の反映と移動			
			ball.vy += gravity;
			ball.x += ball.vx;
			ball.y += ball.vy;
			
			// 画面の上下左右幅の設定
			if(ball.x -ball.radious < 0){
				ball.x = ball.radious;
				ball.vx *= bounce;
			}else if(ball.x + ball.radious > stage.stageWidth){
				ball.x = stage.stageWidth - ball.radious;
				ball.vx *= bounce;
			}
			
			if(ball.y - ball.radious < 0){
				ball.y = ball.radious;
				ball.vy *= bounce;
			}else if(ball.y + ball.radious > stage.stageHeight){
				ball.y = stage.stageHeight - ball.radious;
				ball.vy *= bounce;
			}
			
			// 各ラインとの跳ね返りチェック
			for(var i:uint = 0; i < lineCount; i++){
				checkLine(lines[i]);
			}
		}
		
		private function checkLine(line:Sprite):void{

			// getBoundsメソッドで表示オブジェクトの境界がわかる
			var rect:Rectangle = line.getBounds(this);
			if(rect.left < ball.x && ball.x < rect.right){
				// ラインの横幅内にボールが存在する場合
				
				// ラインの角度から座標回転に必要なcosとsinを設定	
				var angle:Number = line.rotation * Math.PI / 180;
				var cos:Number = Math.cos(angle);
				var sin:Number = Math.sin(angle);
				
				// ボールとラインの距離
				var x1:Number = ball.x - line.x;
				var y1:Number = ball.y - line.y;				
				
				// 座標を回転することで、垂直な跳ね返りとして計算可能になる。
				var x2:Number = cos * x1 + sin * y1;
				var y2:Number = cos * y1 - sin * x1
				
				// 同様に、速度も回転させる
				var vx1:Number = cos * ball.vx + sin * ball.vy;
				var vy1:Number = cos * ball.vy - sin * ball.vx;
				
				// 距離がボールの半径より小さくなった(=衝突)かどうか
				if(y2 > -ball.height / 2 && y2 < vy1){
					
					// ボールをライン上に設定して、垂直成分の速度を反転
					y2 = -ball.height / 2;
					vy1 *= bounce;
					
					// 座標回転元に戻す
					x1 = cos * x2 - sin * y2;
					y1 = cos * y2 + sin * x2;
					// 同様に速度も元に戻す
					ball.vx = cos * vx1 - sin * vy1;
					ball.vy = cos * vy1 + sin * vx1;
					// 移動
					ball.x = line.x + x1;
					ball.y = line.y + y1;
				}
			}
		}

		private function onClick_stage(evt:MouseEvent):void
		{
			isStart = true;
		}
	}
}

import flash.display.Sprite;

class Ball extends Sprite
{
	public var radious:Number;
	private var color:uint;
	public var vx:Number = 0;
	public var vy:Number = 0;

	public function Ball(radius:Number = 40, color:uint = 0xff0000)
	{
		this.radious = radius;
		this.color = color;
		init();
	}

	private function init():void{
		graphics.clear();
		graphics.beginFill(color);
		graphics.drawCircle(0, 0, radious);
		graphics.endFill();
	}
	
	public function setColor(color:uint):void
	{
		this.color = color;
		init();
	}
}


ここまで来るとさすがに難しくなってきました。
一度、プログラムを書いてブログにまとめるくらいじゃなかなか知識として定着しなさそうです。
テトリス(残念な)に続いてなにか自作アプリを作ってみないとダメかもです。


つづく。