Flash/Flex学习笔记(46):正向运动学

所谓"正向运动学"通俗点讲就是把几个连接部件的一端固定起来,另一个端可以自由(向前/向外)运动。比如人的行走,单个下肢可以理解为脚连接小腿,小腿连接大腿,大腿连接腰。行走的过程,相当于二条腿相对固定于腰部,大腿运动驱动小腿,小腿又驱动脚,从而带动整个连接系统的一系列运动。

先来一个基本的关节类Segment:(就是一个圆角矩形+二个小圆圈)

package {
	import flash.display.Sprite;
	import flash.geom.Point;

	public class Segment extends Sprite {

		private var color:uint;
		private var segmentWidth:Number;
		private var segmentHeight:Number;
		public var vx:Number=0;
		public var vy:Number=0;

		public function Segment(segmentWidth:Number,segmentHeight:Number,color:uint=0xffffff) {
			this.segmentWidth=segmentWidth;
			this.segmentHeight=segmentHeight;
			this.color=color;
			init();
		}

		public function init():void {

			// 绘制关节 
			graphics.lineStyle(0);
			graphics.beginFill(color);
			graphics.drawRoundRect(- segmentHeight/2,- segmentHeight/2,segmentWidth+segmentHeight,segmentHeight,segmentHeight,segmentHeight);
			graphics.endFill();

			// 绘制两个“枢轴” 
			graphics.drawCircle(0,0,2);
			graphics.drawCircle(segmentWidth,0,2);
		}

//获得自由端的坐标
		public function getPin():Point {
			var angle:Number=rotation*Math.PI/180;
			var xPos:Number=x+Math.cos(angle)*segmentWidth;
			var yPos:Number=y+Math.sin(angle)*segmentWidth;
			return new Point(xPos,yPos);
		}
	}
}

为了动态控制关节的旋转,再来一个简单的滑块控件类:(下列代码看起来吃力的同学,建议先看Flash/Flex学习笔记(36):自己动手实现一个滑块控件(JimmySilder)

package {
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.geom.Rectangle;
	import flash.events.Event;
	
	public class SimpleSlider extends Sprite {

		private var _width:Number=6;
		private var _height:Number=100;
		private var _value:Number;
		private var _max:Number=100;
		private var _min:Number=0;
		private var _handle:Sprite;
		private var _back:Sprite;
		private var _backWidth:Number=0;
		private var _handleHeight:Number=20;
		private var _backColor:uint=0xcccccc;
		private var _backBorderColor:uint=0x999999;
		private var _handleColor:uint=0x000000;
		private var _handleBorderColor:uint=0xcccccc;
		private var _handleRadius:Number=2;
		private var _backRadius:Number=2;

		public function SimpleSlider(min:Number = 0, max:Number = 100, value:Number = 100 ) {
			_min=min;
			_max=max;
			_value=Math.min(Math.max(value,min),max);
			init();
		}

		private function init():void {
			_back = new Sprite () ;
			addChild(_back);
			_handle = new Sprite () ;
			_handle.buttonMode=true;
			addChild(_handle);
			_handle.addEventListener( MouseEvent.MOUSE_DOWN , MouseDownHandler );
			draw();
			updatePosition();
		}

		private function draw():void {
			drawBack();
			drawHandle();
		}

		private function drawBack():void {
			_back.graphics.clear();
			_back.graphics.beginFill( _backColor );
			_back.graphics.lineStyle( 0, _backBorderColor );
			_back.graphics.drawRoundRect( 0, 0, _backWidth , _height , _backRadius , _backRadius );
			_back.graphics.endFill();
			_back.x=_width/2-_backWidth/2;
		}

		private function drawHandle():void {
			_handle.graphics.clear();
			_handle.graphics.beginFill( _handleColor );
			_handle.graphics.lineStyle( 0, _handleBorderColor );
			_handle.graphics.drawRect( 0, 0, _width , _handleHeight );
			_handle.graphics.endFill();
		}

		private function updatePosition():void {
			var handleRange:Number=_height-_handleHeight;
			var valueRange:Number=_max-_min;
			_handle.y = handleRange - ( _value - _min ) / valueRange * handleRange ;
		}

		private function updateValue():void {
			var handleRange:Number=_height-_handleHeight;
			var valueRange:Number=_max-_min;
			_value = ( handleRange - _handle.y ) / handleRange * valueRange + _min ;
			dispatchEvent( new Event ( Event.CHANGE ));
		}

		private function MouseUpHandler( e:MouseEvent ):void {
			stage.removeEventListener( MouseEvent.MOUSE_MOVE , MouseMoveHandler );
			stage.removeEventListener( MouseEvent.MOUSE_UP , MouseUpHandler );
			_handle.stopDrag();
		}

		private function MouseDownHandler( e:MouseEvent ):void {
			stage.addEventListener( MouseEvent.MOUSE_MOVE , MouseMoveHandler );
			stage.addEventListener( MouseEvent.MOUSE_UP , MouseUpHandler );
			_handle.startDrag( false , new Rectangle ( 0, 0, 0, _height - _handleHeight ));
		}

		private function MouseMoveHandler( e:MouseEvent ):void {
			updateValue();
		}

		public function invalidate():void {
			draw();
		}

		public function move( x:Number , y:Number ):void {
			this.x=x;
			this.y=y;
		}

		public function setSize( w:Number , h:Number ):void {
			_width=w;
			_height=h;
			draw();
		}

		public function set backBorderColor( n:uint ):void {
			_backBorderColor=n;
			draw();
		}

		public function get backBorderColor():uint {
			return _backBorderColor;
		}

		public function set backColor( n:uint ):void {
			_backColor=n;
			draw();
		}

		public function get backColor():uint {
			return _backColor;
		}

		public function set backRadius( n:Number ):void {
			_backRadius=n;
		}

		public function get backRadius():Number {
			return _backRadius;
		}

		public function set backWidth( n:Number ):void {
			_backWidth=n;
			draw();
		}

		public function get backWidth():Number {
			return _backWidth;
		}

		public function set handleBorderColor( n:uint ):void {
			_handleBorderColor=n;
			draw();
		}

		public function get handleBorderColor():uint {
			return _handleBorderColor;
		}

		public function set handleColor( n:uint ):void {
			_handleColor=n;
			draw();
		}

		public function get handleColor():uint {
			return _handleColor;
		}
		public function set handleRadius( n:Number ):void {
			_handleRadius=n;
			draw();
		}
		public function get handleRadius():Number {
			return _handleRadius;
		}
		public function set handleHeight( n:Number ):void {
			_handleHeight=n;
			draw();
			updatePosition();
		}
		public function get handleHeight():Number {
			return _handleHeight;
		}
		override public function set height( n:Number ):void {
			_height=n;
			draw();
		}
		override public function get height():Number {
			return _height;
		}
		public function set max( n:Number ):void {
			_max=n;
			updatePosition();
		}
		public function get max():Number {
			return _max;
		}
		public function set min( n:Number ):void {
			_min=n;
			updatePosition();
		}
		public function get min():Number {
			return _min;
		}
		public function set value( n:Number ):void {
			_value=n;
			_value=Math.min(_max,Math.max(_value,_min));
			updatePosition();
		}
		public function get value():Number {
			return _value;
		}
		override public function set width( n:Number ):void {
			_width=n;
			draw();
		}
		override public function get width():Number {
			return _width;
		}
	}
}

基本测试:

var segment:Segment=new Segment(100,20);
addChild(segment);
segment.x=50;
segment.y=120;

var slider:SimpleSlider=new SimpleSlider(-90,90,0);
addChild(slider);
slider.x=200;
slider.y=70;

slider.addEventListener(Event.CHANGE,onChange);

function onChange(event:Event):void {
	segment.rotation=slider.value;
}

双关节运动测试:

package {
	import flash.display.Sprite;
	import flash.events.Event;
	public class TwoSegments extends Sprite {
		private var slider0:SimpleSlider;		
		private var slider1:SimpleSlider;
		private var segment0:Segment;
		private var segment1:Segment;
		
		public function TwoSegments() {
			init();
		}
		private function init():void {
			segment0=new Segment(100,20);
			addChild(segment0);
			segment0.x=50;
			segment0.y=150;
			segment1=new Segment(100,20);
			addChild(segment1);
			
			//关键:segment1的固定端连接到segment0的自由端
			segment1.x=segment0.getPin().x;
			segment1.y=segment0.getPin().y;
			
			slider0=new SimpleSlider(-90,90,0);
			addChild(slider0);
			slider0.x=320;
			slider0.y=90;
			slider0.addEventListener(Event.CHANGE,onChange);
			slider1=new SimpleSlider(-90,90,0);
			addChild(slider1);
			slider1.x=340;
			slider1.y=90;
			slider1.addEventListener(Event.CHANGE,onChange);
		}
		private function onChange(event:Event):void {
			segment0.rotation=slider0.value;
			segment1.rotation=slider1.value;
			segment1.x=segment0.getPin().x;
			segment1.y=segment0.getPin().y;
		}
	}
}

如果把segment0与segment1分别看做人的胳膊与手臂,上面这个示例显然有二个地方不自然:

1.没有人的(前)手臂向下做-90度的弯曲(除非脱臼)

2.人的上肢整体向上抬时,手臂会随着胳膊一起绕肩关节向上旋转,而不应该一直固定于某个角度

修正的方法很简单,onChange改成下面这样:

private function onChange(event:Event):void {
			segment0.rotation=slider0.value;
			segment1.rotation=slider1.value + segment0.rotation;//注意这行
			segment1.x=segment0.getPin().x;
			segment1.y=segment0.getPin().y;
		}

同时限制一下slider1的角度范围,改成下面这样:

slider1=new SimpleSlider(-160,0,0);

单腿原地“踢”模拟

package {
	import flash.display.Sprite;
	import flash.events.Event;
	public class Walking1 extends Sprite {
		private var segment0:Segment;
		private var segment1:Segment;
		private var cycle:Number=0;
		private var offset:Number = -Math.PI/2;//小腿的运动看上去应该滞后于大腿,所以需要加入反向偏移量
		
		public function Walking1() {
			init();
			trace(Math.PI/180);
			trace(0.05*180/Math.PI);
		}
		private function init():void {
			segment0=new Segment(100,20);
			addChild(segment0);
			segment0.x=200;
			segment0.y=200;
			segment1=new Segment(100,20);
			addChild(segment1);
			segment1.x=segment0.getPin().x;
			segment1.y=segment0.getPin().y;
			addEventListener(Event.ENTER_FRAME,onEnterFrame);
		}
		private function onEnterFrame(event:Event):void {
			cycle+=.05;
			var angle0:Number=Math.sin(cycle)*45 + 90;//-45到45整体加上90度以后,就变成45到135,即:大腿垂直方向左右摆动45度
			var angle1:Number = Math.sin(cycle + offset) * 45  + 45;//即:小腿相对大腿末端做0-90度的正向旋转。建议大家尝试修改一下这里的+45值的大小,看看效果有什么不同
			segment0.rotation=angle0;
			segment1.rotation=segment0.rotation+angle1;
			segment1.x=segment0.getPin().x;
			segment1.y=segment0.getPin().y;
		}
	}
}

双腿原地行走:

package {
	import flash.display.Sprite;
	import flash.events.Event;
	public class Walking4 extends Sprite {
		private var segment0:Segment;
		private var segment1:Segment;
		private var segment2:Segment;
		private var segment3:Segment;
		
		private var cycle:Number=0;
		private var offset:Number=- Math.PI/2;//小腿的运动看上去应该滞后于大腿,所以需要加入反向偏移量

		public function Walking4() {
			init();			
		}
		private function init():void {
			
			segment0=new Segment(100,35);//第一条大腿
			addChild(segment0);
			segment0.x=200;
			segment0.y=50;
			
			segment1=new Segment(100,20);			
			addChild(segment1);
			segment1.x=segment0.getPin().x;//第一条小腿连接到第一条大腿
			segment1.y=segment0.getPin().y;
			
			segment2=new Segment(100,35);//第二条大腿
			segment2.x = segment0.x;//第二条大腿与第一条大腿坐标相同,视觉效果上看,就象都固定在腰部
			segment2.y = segment0.y;
			addChild(segment2);
			
			
			segment3=new Segment(100,20);			
			addChild(segment3);
			segment3.x=segment2.getPin().x;//第二条小腿连接到第二条大腿
			segment3.y=segment2.getPin().y;
			
			addEventListener(Event.ENTER_FRAME,EnterFrameHandler);
		}
		
		private function EnterFrameHandler(event:Event):void {
			walk(segment0, segment1, cycle); 
			walk(segment2, segment3, cycle + Math.PI);//注意这里的:+Math.PI,如果不加这个,二条腿的频率/角度完全相同,将重叠在一起,加上180度以后,正好反相过来,一条腿在前,另一条腿在后
			cycle += .05;
		}
		
		//把"走"的动作封装起来
		private function walk(segA:Segment, segB:Segment, cyc:Number):void {
			var angleA:Number=Math.sin(cyc)*45+90;
			var angleB:Number=Math.sin(cyc+offset)*45+45;
			segA.rotation=angleA;
			segB.rotation=segA.rotation+angleB;
			segB.x=segA.getPin().x;
			segB.y=segA.getPin().y;
		}
	}
}

加入滑块控制条后的样子:

package {
	import flash.display.Sprite;
	import flash.events.Event;
	public class Walking5 extends Sprite {
		private var segment0:Segment;
		private var segment1:Segment;
		private var segment2:Segment;
		private var segment3:Segment;
		private var speedSlider:SimpleSlider;
		private var thighRangeSlider:SimpleSlider;
		private var thighBaseSlider:SimpleSlider;
		private var calfRangeSlider:SimpleSlider;
		private var calfOffsetSlider:SimpleSlider;
		private var cycle:Number=0;
		
		public function Walking5() {
			init();
		}

		private function init():void {
			segment0=new Segment(100,30);
			addChild(segment0);
			segment0.x=200;
			segment0.y=100;
			segment1=new Segment(100,20);
			addChild(segment1);
			segment1.x=segment0.getPin().x;
			segment1.y=segment0.getPin().y;
			segment2=new Segment(100,30);
			addChild(segment2);
			segment2.x=200;
			segment2.y=100;
			segment3=new Segment(100,20);
			addChild(segment3);
			segment3.x=segment2.getPin().x;
			segment3.y=segment2.getPin().y;
			
			//控制速度的滑块
			speedSlider=new SimpleSlider(0,0.5,0.11);
			addChild(speedSlider);
			speedSlider.x=10;
			speedSlider.y=10;
			
			//控制大腿能分开的最大角度
			thighRangeSlider=new SimpleSlider(0,90,45);
			addChild(thighRangeSlider);
			thighRangeSlider.x=30;
			thighRangeSlider.y=10;
			
			//大腿旋转的偏移量
			thighBaseSlider=new SimpleSlider(0,180,90);
			addChild(thighBaseSlider);			
			thighBaseSlider.x=50;
			thighBaseSlider.y=10;
			
			//小腿旋转的偏移量
			calfRangeSlider=new SimpleSlider(0,90,45);
			addChild(calfRangeSlider);			
			calfRangeSlider.x=70;
			calfRangeSlider.y=10;
			
			//小腿相对大腿滞后的偏移量
			calfOffsetSlider=new SimpleSlider(-3.14,3.14,-1.57);
			addChild(calfOffsetSlider);			
			calfOffsetSlider.x=90;
			calfOffsetSlider.y=10;

			addEventListener(Event.ENTER_FRAME, EnterFrameHandler);
		}
		
		private function EnterFrameHandler(e:Event):void {
			walk(segment0, segment1, cycle);
			walk(segment2, segment3, cycle + Math.PI);
			cycle+=speedSlider.value;
		}
		private function walk(segA:Segment, segB:Segment,cyc:Number):void {
			var angleA:Number = Math.sin(cyc) *	thighRangeSlider.value + thighBaseSlider.value;
			var angleB:Number = Math.sin(cyc +calfOffsetSlider.value) *	calfRangeSlider.value +	calfRangeSlider.value;
			segA.rotation=angleA;
			segB.rotation=segA.rotation+angleB;
			segB.x=segA.getPin().x;
			segB.y=segA.getPin().y;
		}
	}
}

真正的行走:

package {
	import flash.display.Sprite;
	import flash.display.StageScaleMode;
	import flash.display.StageAlign;
	import flash.display.Stage;
	import flash.events.Event;
	import flash.geom.Point;
	public class RealWalk extends Sprite {
		
		private var segment0:Segment;//大腿1
		private var segment1:Segment;//小腿1
		private var segment2:Segment;//大腿2
		private var segment3:Segment;//小腿2
		
		//各控制滑块
		private var speedSlider:SimpleSlider;
		private var thighRangeSlider:SimpleSlider;
		private var thighBaseSlider:SimpleSlider;
		private var calfRangeSlider:SimpleSlider;
		private var calfOffsetSlider:SimpleSlider;
		private var gravitySlider:SimpleSlider;
		
		private var cycle:Number=0;
		private var vx:Number=0;
		private var vy:Number=0;

		public function RealWalk() {
			init();
		}

		private function init():void {
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			segment0=new Segment(50,15);
			addChild(segment0);
			segment0.x=200;
			segment0.y=100;
			
			segment1=new Segment(50,10);
			addChild(segment1);
			segment1.x=segment0.getPin().x;
			segment1.y=segment0.getPin().y;
			
			segment2=new Segment(50,15);
			addChild(segment2);
			segment2.x=200;
			segment2.y=100;
			
			segment3=new Segment(50,10);
			addChild(segment3);
			segment3.x=segment2.getPin().x;
			segment3.y=segment2.getPin().y;
			
			speedSlider=new SimpleSlider(0,0.3,0.12);
			addChild(speedSlider);
			speedSlider.x=10;
			speedSlider.y=10;
			
			thighRangeSlider=new SimpleSlider(0,90,45);
			addChild(thighRangeSlider);
			thighRangeSlider.x=30;
			thighRangeSlider.y=10;
			
			thighBaseSlider=new SimpleSlider(0,180,90);
			addChild(thighBaseSlider);
			thighBaseSlider.x=50;
			thighBaseSlider.y=10;
			
			calfRangeSlider=new SimpleSlider(0,90,45);
			addChild(calfRangeSlider);
			calfRangeSlider.x=70;
			calfRangeSlider.y=10;
			
			calfOffsetSlider=new SimpleSlider(-3.14,3.14,-1.57);
			addChild(calfOffsetSlider);
			calfOffsetSlider.x=90;
			calfOffsetSlider.y=10;
			
			gravitySlider=new SimpleSlider(0,1,0.2);
			addChild(gravitySlider);
			gravitySlider.x=110;
			gravitySlider.y=10;
			
			addEventListener(Event.ENTER_FRAME, EnterFrameHandler);
		}
		
		private function EnterFrameHandler(event:Event):void {
			doVelocity();
			walk(segment0, segment1, cycle);
			walk(segment2, segment3, cycle + Math.PI);
			cycle+=speedSlider.value;
			checkFloor(segment1);
			checkFloor(segment3);
			checkWalls();
		}
		
		//行走姿态的处理
		private function walk(segA:Segment, segB:Segment,cyc:Number):void {
			var foot:Point=segB.getPin();
			var angleA:Number = Math.sin(cyc) *thighRangeSlider.value +thighBaseSlider.value;
			var angleB:Number = Math.sin(cyc +calfOffsetSlider.value) *calfRangeSlider.value +calfRangeSlider.value;
			segA.rotation=angleA;
			segB.rotation=segA.rotation+angleB;
			segB.x=segA.getPin().x;
			segB.y=segA.getPin().y;
			segB.vx=segB.getPin().x-foot.x;
			segB.vy=segB.getPin().y-foot.y;
		}
		
		//下肢的速度处理
		private function doVelocity():void {
			vy+=gravitySlider.value;
			//因为小腿是跟着大腿的,所以只要处理大腿的速度即可
			segment0.x+=vx;
			segment0.y+=vy;
			segment2.x+=vx;
			segment2.y+=vy;
		}
		
		private function checkFloor(seg:Segment):void {
			var yMax:Number=seg.getBounds(this).bottom;
			//如果最下面的小腿超出了舞台下边界
			if (yMax>stage.stageHeight) {
				var dy:Number=yMax-stage.stageHeight;
				//将所有的关节(大腿和小腿)全部上移,以防止两条腿超出舞台下边界
				segment0.y-=dy;
				segment1.y-=dy;
				segment2.y-=dy;
				segment3.y-=dy;
				//速度反弹
				vx-=seg.vx;
				vy-=seg.vy;
			}
		}
		
		//屏幕环绕
		private function checkWalls():void {
			var w:Number=stage.stageWidth+200;
			if (segment0.x>stage.stageWidth+100) {
				segment0.x-=w;
				segment1.x-=w;
				segment2.x-=w;
				segment3.x-=w;
			} else if (segment0.x < 100*-1) {
				segment0.x+=w;
				segment1.x+=w;
				segment2.x+=w;
				segment3.x+=w;
			}
		}
	}
}
posted @ 2010-04-30 15:25  菩提树下的杨过  阅读(2047)  评论(6编辑  收藏  举报