Ismerkedés az ActionScript 3-mal, 5. rész – Kamerával irányított labda

Igen, és sorozatunk végére érkeztünk, jutalomképpen egy igen látványos és zsír dolgot fogunk megvalósítni: a kamera inputtal fogjuk irányítani az előző részekben felprogramozott labdánkat.
Szükségünk lesz egy új osztályra MotionDetectorLogic néven, ami bemenetként megkapja a hozzá tartozó videó objektumot, amin a változásokat figyelnie kell, továbbá kapni fog egy Bitmap objektumot is, hogy kirajzolhassuk a változásokat, ez debughoz elengedhetetlen.

Mielőtt a billentyűk közé csapnánk, fogjuk le magunkat: most érte el a program azt a bonyolultsági fokot, hogy nem szabad csak úgy nekifognunk. Egy legalább 3 osztályból álló programot már meg kell terveznünk ( papíron és fejben ), és föl kell rajzolnunk ( UML rajzoló progiban, pl AmaterasUML Eclipse alá ), és csak ezután lehet nekivágni. Az igazi programozó onnan ismerszik meg, hogy az UML rajza és a kész program között legfeljebb 40 százalékos az eltérés :)

Lesz tehát egy MotionDetectorLogic osztályunk, ami tartalmazni fog egy setVideo függvényt, amin keresztül megkapja a videót, egy setBitmap függvényt, amin keresztül az „output”ját kapja meg, akkor kell egy getPoints függvény, ami kiszámolja a videó objektum két állapota közötti eltéréseket, és az eltérő pontokat egy tömbben tárolja, meg egy drawDifference függvény is kell, ami kirajzolja az eltéréseket, hogy lássuk ha valami rossz ott, továbbá érzékenységálllításkor is hasznos.

A FootballGame vezérlőosztály annyiban módosul, hogy a szélesség és magasság és a sugár konstansokat a vezérlőosztályból fogja kirántani.

Mindent egybevetve az UML diagram így fog kinézni:

Lássuk a kódot:

MotionDetectorLogic.as:

//begin
//
//
package logic
{
	//
	//
	//
	import flash.display.BitmapData;
	import flash.display.Bitmap;
	import flash.media.Video;
	//
	import flash.geom.Rectangle;
	import flash.geom.ColorTransform;
	//

A ColorTransform a színtranszformációhoz kell, mert a változásokat egyre halványuló zöld négyzetekkel szeretnénk jegyezni, és a halványításnak ez a legegyszerűbb módja. A Rectangle osztályt pedig a BitmapData használja téglalap kijelölésekhez.

	//
	//
	//
	public class MotionDetectorLogic
	{
		//
		//
		//
		private var root:Object;
		//
		private var difColTR:ColorTransform;
		private var actBMP:BitmapData;
		private var oldBMP:BitmapData;
		private var difBMP:BitmapData;
		private var myBMP:Bitmap;
		private var myVID:Video;
		//
		private var SENSITIVITY:int = 700000;
		private var BLOCKSIZE:int = 10;

Az actBMP az aktuális, míg az oldBMP az eggyel előző állapotát tárolja a Videónak, a difBMP-be pedig az eltérést rajzoljuk. A SENSITIVITY a mozgásdetekció érzékenysélge, a BLOCKSIZE pedig a felbontása.

		//
		//
		//
		public function MotionDetectorLogic ( myRoot:Object ):void
		{
			//
			root = myRoot;
			//
			difColTR = new ColorTransform( 1 , 1 , 1 , .7 , 0 , 0 , 0 , 0 );
			//
			actBMP = new BitmapData( 320 , 240 , false , 0x000000 );
			oldBMP = new BitmapData( 320 , 240 , false , 0x000000 );
			difBMP = new BitmapData( 320 , 240 , true , 0x000000 );
			//
			addLog( "new MotionDetectorLogic " );
			//
		}
		//
		//
		//
		public function setVideo ( vidOBJ:Video ):void
		{
			//
			myVID = vidOBJ;
			//
			addLog( "MotionDetectorLogic.setVideo " + vidOBJ );
			//
		}
		//
		//
		//
		public function setBitmap ( bmpOBJ:Bitmap ):void
		{
			//
			myBMP = bmpOBJ;
			myBMP.bitmapData = difBMP;
			//
			addLog( "MotionDetectorLogic.setBitmap " + bmpOBJ );
			//
		}

Ezek értelemszerűek, illetve annyi érdekesség van, hogy a difBMP-nek kell egyedül transzparensne k lennie, ezért lett true a transzparencia, illetve itt a Bitmap „classunk” üresen jött létre a vezérlőkódban, így itt kell hozzárendelnünk a BitmapData – t.

		//
		//
		//
		public function getPoints ( ):Array
		{
			//
			actBMP.draw( myVID );
			//
			var diffPoints:Array = [ ];
			//
			for ( var a:int = 0 ; a < actBMP.width ; a += BLOCKSIZE )
			 for ( var b:int = 0 ; b < actBMP.height ; b += BLOCKSIZE )
			 {
			 	//
			 	var oldPx:uint = oldBMP.getPixel( a , b );
			 	var newPx:uint = actBMP.getPixel( a , b );
			 	//
			 	if ( ( oldPx - SENSITIVITY ) > newPx || ( oldPx + SENSITIVITY ) < newPx ) 
			 		diffPoints.push( { x:a , y:b } );
			 	//
			 }
			//
			oldBMP = actBMP.clone( );
			drawDifference( diffPoints );
			//
			//addLog( "MotionDetectorLogic.getPoints " + diffPoints );
			//
			return diffPoints;
			//
		}

Ez a függvény végzi a tényleges melót, első lépésként „leképezem” a videót azt actBMP-re, és végigpörgetem a pontjait a megadott lépésközzel, ott pedig megnézem, hogy az oldBMP-hez képest a vizsgált pontok változása tűréshatáron belül-e van ( a tűréshatár nélülözhetetlen, lévén a fény meglehetősen analóg jelenség :) ), és ha igen, akkor berakom a tömbömbe a többi pont mellé. Mielőtt visszatérnék, még fölrajzolom a pontokat a difBMP-be.

		//
		//
		//
		public function drawDifference ( diffPoints:Array ):void
		{
			//
			difBMP.colorTransform( difBMP.rect , difColTR );
			//
			for ( var a:int = 0 ; a < diffPoints.length ; ++a )
			{
				//
				var p:Object = diffPoints[a];
				var rect:Rectangle = new Rectangle( p.x , p.y , BLOCKSIZE , BLOCKSIZE ); 
				difBMP.fillRect( rect , 0xff00ff00 ); 
				//
			}
			//
			//addLog( "MotionDetectorLogic.drawDifference" );
			//
		}

Először halványítok egyett a differencia Bitmap-en, aztán simán végigpörgök a detektált pontokon, és kihányok a difBMP-re egy lépésköz*lépésköz-ös zöld négyzetet.

		//
		//
		//
		public function addLog ( logText:String ):void
		{
			//
			this.root.addLog( logText );
			//
		}
		//
		//
		//
	}
	//
	//
	//
}
//
//
//end

FootballGame.as:

//begin
//
//
package 
{
	//
	//
	//
	import flash.media.Camera;
	import flash.media.Video;
	import flash.display.Bitmap;
	import flash.display.MovieClip;
	import flash.display.Loader;
	import flash.display.StageScaleMode;
	import flash.display.StageAlign;
	import flash.media.Sound;
	import flash.net.URLRequest;
	import flash.events.*
	import flash.utils.*;
	//
	import logic.MotionDetectorLogic;
	import logic.InteractivePointLogic;
	import flash.display.Sprite;

Na itt aztán van mit importálni, kelleni fog nekünk Kamera, Videó, az új MotionDetector logikánk, meg még az előző cuccok.

	//
	//
	//
	public class FootballGame extends Sprite
	{
		//
		//
		private var moveTimer:Timer;
		//
		private var hasSkin:Boolean;
		private var hasSound:Boolean;
		private var ballSkinLO:Loader;
		private var ballSkinSP:Sprite;
		private var ballPopSND:Sound;
		private var ballPoint:InteractivePointLogic;
		//
		public var RADIUS:Number = 20;
		public var WIDTH:int = 320;
		public var HEIGHT:int = 240;
		//
		private var xspeed:Number;
		private var yspeed:Number;
		private var oldx:Number;
		private var oldy:Number;
		//
		private var motDetOBJ:MotionDetectorLogic;
		private var movieVID:Video;
		private var movieBMP:Bitmap;
		private var movieCAM:Camera;

Fölvettem a szükséges változókat a Videohoz, Kamerához és Bitmap-hez, továbbá új konstansokat RADIUS, WIDTH, HEIGHT névvel, ugyanis már sok helyen használom őket, sőt, az InteractivePointLogic is kinyúl értük.

		//
		//
		//
		public function FootballGame ( )
		{
			//
			stage.frameRate = 25;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP;
			//
			movieVID = new Video( );
			movieBMP = new Bitmap( );
			movieCAM = Camera.getCamera( );
			movieVID.attachCamera( movieCAM );
			addChild( movieVID );
			addChild( movieBMP );
			//
			motDetOBJ = new MotionDetectorLogic( this );
			motDetOBJ.setVideo( movieVID );
			motDetOBJ.setBitmap( movieBMP );
			//
			loadSkin( );
			loadSound( );
			//
			addLog( "new FootballGame" );
			//
		}

A konstruktorból kivettem a háttérrajzolást, a Videó fogja őt helyettesíteni, és inicializálom a MotionDetectorLogic-ot.

		//
		//
		//
		public function loadSkin ( ):void
		{
			//
			ballSkinSP = new Sprite( );
			addChild( ballSkinSP );
			//
			var urlRequest:URLRequest = new URLRequest( "soccerball.gif" );
			ballSkinLO = new Loader( );
			ballSkinLO.contentLoaderInfo.addEventListener( Event.COMPLETE , skinLoaded );
			ballSkinLO.contentLoaderInfo.addEventListener( IOErrorEvent.IO_ERROR , ioErrorHandler );
			ballSkinLO.load( urlRequest );
			ballSkinSP.addChild( ballSkinLO );
			//
			addLog( "FootballGame.loadSkin" );
			//
		}
		//
		//
		//
		public function loadSound ( ):void
		{
			//
			var urlRequest:URLRequest = new URLRequest( "pop.mp3" );
			ballPopSND = new Sound( );
			ballPopSND.addEventListener( Event.COMPLETE , soundLoaded );
                        ballPopSND.addEventListener( IOErrorEvent.IO_ERROR , ioErrorHandler );
			ballPopSND.load( urlRequest );
			//
			addLog( "FootballGame.loadSoun" );
			//
		}
		//
		//
		//
		public function ioErrorHandler ( eventOBJ:Event ):void
		{
			//
			addLog( "ioError: " + eventOBJ );
			//
		}
		//
		//
		//
		public function skinLoaded ( eventOBJ:Event ):void
		{
			//
			ballSkinLO.x = -RADIUS;
			ballSkinLO.y = -RADIUS;
			ballSkinLO.width = 2*RADIUS;
			ballSkinLO.height = 2*RADIUS;
			hasSkin = true;
			initLogic( );
			//
			addLog( "FootballGame.skinLoaded" );
			//
		}
		//
		//
		//
		public function soundLoaded ( eventOBJ:Event ):void
		{
			//
			hasSound = true;
			initLogic( );
			//
			addLog( "FootballGame.soundLoaded" );
			//
		}
		//
		//
		//
		public function initLogic ( ):void
		{
			//
			if ( hasSkin && hasSound )
			{
				//
				ballPoint = new InteractivePointLogic( this );
				ballPoint.xpos = 20;
				ballPoint.ypos = 20;
				ballPoint.xspeed = Math.random( )*5;
				ballPoint.yspeed = Math.random( )*5;
				ballPoint.setSkin( ballSkinSP );
				ballPoint.setSound( ballPopSND );
				//
				moveTimer = new Timer( 25 );
				moveTimer.addEventListener( TimerEvent.TIMER , step );
				moveTimer.start( );
				//
			}
			//
			addLog( "FootbalGame.initLogic " + hasSkin + " " + hasSound );
			//
		}
		//
		//
		//
		public function step ( eventOBJ:Event ):void
		{
			//
			var diffARR:Array = motDetOBJ.getPoints( );
			//
			for ( var a:int = 0 ; a < diffARR.length ; ++a )
			{
				//
				var dx:Number = ballPoint.xpos - diffARR[a].x;
				var dy:Number = ballPoint.ypos - diffARR[a].y;
				var dr:Number = Math.sqrt( dx*dx + dy*dy );
				//
				if ( dr < RADIUS )
				{
					//
					ballPoint.xspeed = dx;
					ballPoint.yspeed = dy;
					break;
					//
				}
				//
			}
			//
			ballPoint.step( );
			//
		}

A másik lényeges dolgot a step függvény végzi el, lekérdezi a válozásokat a MotionDetector-tól, majd csinál egy primitív ütközésvizsgálatot: megnézi a labda középpontja és a különbségpontok közötti távolságot, és ha a táv kisebb mint a labda sugara, akkor ütközés van: átadja a pointLogic-nak x és yspeed értékeket, majd leállítja a for ciklust: fölösleges tovább pörgetni, és életszerűbb a hatás is.

		//
		//
		//
		public function dragBall ( ):void
		{
			//
			addEventListener( MouseEvent.MOUSE_MOVE , moveBall );
			moveTimer.stop( );
			//
			addLog( "FootbalGame.dragBall" );
			//
		}
		//
		//
		//
		public function releaseBall ( ):void
		{
			//
			removeEventListener( MouseEvent.MOUSE_MOVE , moveBall );
			ballPoint.xspeed = xspeed;
			ballPoint.yspeed = yspeed;
			moveTimer.start( );
			//
			addLog( "FootbalGame.releaseBall" );
			//
		}
		//
		//
		//
		public function moveBall ( eventOBJ:MouseEvent ):void
		{
			//
			ballPoint.xpos = mouseX;
			ballPoint.ypos = mouseY;
			ballPoint.updateSkin( );
			//
			xspeed = mouseX - oldx;
			yspeed = mouseY - oldy;
			//
			oldx = mouseX;
			oldy = mouseY;
			//
			addLog( "FootbalGame.moveBall" + mouseX + " " + mouseY );
			//
		}	
		//
		//
		//
		public function addLog( logText:String ):void
		{
			//
			trace( logText );
			//
		}
		//
		//
		//
	}
	//
	//
	//
}
//
//
//end

A többi pedig megmaradt múltkorról.

InteractivePointLogic:

//begin
//
//
package logic
{
	//
	//
	//
	import flash.display.Sprite;
	import flash.display.Loader;
	import flash.media.Sound;
	import flash.events.*;
	//
	//
	//
	public class InteractivePointLogic
	{
		//
		//
		//
		private var root:Object;
		private var gravity:Number = 0.5;
		private var skinSP:Sprite;
		private var ballSND:Sound;
		//
		public var xpos:Number;
		public var ypos:Number;
		public var xspeed:Number;
		public var yspeed:Number;
		//
		//
		//
		public function InteractivePointLogic ( myRoot:Sprite )
		{
			//
			root = myRoot;
			addLog( "new InteractivePointLogic" );
			//
		}
		//
		//
		//
		public function setSkin ( mySP:Sprite ):void
		{
			//
			skinSP = mySP;
			skinSP.addEventListener( MouseEvent.MOUSE_DOWN , mouseDownHandler );
			skinSP.addEventListener( MouseEvent.MOUSE_UP , mouseUpHandler );
			//
			addLog( "InteractivePointLogic.setSkin " + mySP );
			//
		}
		//
		//
		//
		public function updateSkin ( ):void
		{
			//
			skinSP.x = xpos;
			skinSP.y = ypos;
			skinSP.rotation += xspeed;
			//
			//addLog( "InteractivePointLogic.updateSkin" );
			//
		}
		//
		//
		//
		public function setSound ( mySND:Sound ):void
		{
			//
			ballSND = mySND;
			//
			addLog( "InteractivePointLogic.setSound " + mySND );			
			//
		}
		//
		//
		//
		public function step ( ):void
		{
			//
			yspeed += gravity;
			//
			if ( xpos + xspeed > root.WIDTH - root.RADIUS ) 
			{
				//
				ballSND.play( );
				xspeed *= -.9;
				//
			}
			//
			if ( xpos + xspeed < 0 + root.RADIUS ) 
			{
				//
				ballSND.play( );
				xspeed *= -.9;
				//
			}
			//
			if ( ypos + yspeed > root.HEIGHT - root.RADIUS - 20 ) 
			{
				//
				ballSND.play( );
				yspeed -= gravity;
				yspeed *= -.9;
				//
			}
			//
			if ( ypos + yspeed < 0 + root.RADIUS )
			{
				//
				ballSND.play( );
				yspeed *= -.9;
				//
			}				
			//
			xpos += xspeed;
			ypos += yspeed;
			//
			updateSkin( );
			//
			//addLog( "InteractivePointLogic.step" );			
			//
		}
		//
		//
		//
		public function mouseDownHandler ( eventOBJ:MouseEvent ):void
		{
			//
			root.dragBall( );
			//
		}
		//
		//
		//
		public function mouseUpHandler ( eventOBJ:MouseEvent ):void
		{
			//
			root.releaseBall( );
			//
		}
		//
		//
		//
		private function addLog ( logText:String ):void
		{
			//
			root.addLog( logText );
			//
		}
		//
		//
		//
	}
	//
	//
	//
}
//
//
//end

Itt nem sok változás történt: bekerültek a apaosztálybeli konstansok. ( a father változót root-ra neveztem, mivel ezek már „sima”, nem DisplayObjectből származtatott osztályok, így nincs ütközés )

Élesben itt látható: motionball

Forrás: motionball

Ha túl érzékeny/nem érzékel semmit a kód, akkor MotionDetectorLogic SENSITIVITY konstansát kell birizgálni. Ha frankó az érzékenység, akkor ki lehet kommentezni a MotionDetectorLogic osztály getPoints függvényéből a drawDifference-t, és mehettek „vakon” :)

Jó szórakozást, nekem a fejelés már profin megy :)
/hmm, most látom csak, hogy a vezérlőosztály függvényeit jobb lett volna privátként értelmezni az egéresemények kivételével, most már mindegy :) /

MilGra

HOZZÁSZÓLOK A CIKKHEZ

Kérjük, írja be véleményét!
írja be ide nevét