- Crayon Physics
記得兩三年前才在youtube上看到MIT的Assist Sketch Understanding System and Operation,結果現在這個設計已經商品化了,你可以參觀下面的官方網站並且下載demo來玩玩看。
記得兩三年前才在youtube上看到MIT的Assist Sketch Understanding System and Operation,結果現在這個設計已經商品化了,你可以參觀下面的官方網站並且下載demo來玩玩看。
我想這是在OOP裡一個很大的tradeoff,我們必須在效能與框架設計之間取得平衡,考慮下面這一個簡單的效果:
我們手邊已經有一個CircleMotion的Class,如下:
/* CircleMotion.as */
package com.xinyu.motion{
import flash.utils.Timer;
import flash.events.TimerEvent;
import flash.display.DisplayObject;
import com.xinyu.science.Science;
public class CircleMotion2D extends Motion{
private var _timer:Timer;
private var _radian:Number = 0;
private var _radius:Number;
private var _speed:Number;
private var _dir:int;
private var _xOffset:Number;
private var _yOffset:Number;
private var _obj:DisplayObject;
public static const CW:int = 1;
public static const CCW:int = -1;
public function CircleMotion2D(obj:DisplayObject, xOffset:Number = 0 ,yOffset:Number = 0, radius:Number = 50, speed:Number = 0.1, fps:Number = 30, dir:Number = CircleMotion2D.CW, radOffset:int = 0){
_obj = obj;
_radius = radius;
_speed = Math.min(1, Math.max(speed, 0.001));
_dir = dir;
_xOffset = xOffset;
_yOffset = yOffset;
_radian = Science.degToRad(radOffset%360);
_timer = new Timer(1000/fps);
_timer.addEventListener(TimerEvent.TIMER, onTimer);
}
private function onTimer(event:TimerEvent):void{
_obj.x = _radius*Math.cos(_radian) + _xOffset - _radius;
_obj.y = _radius*Math.sin(_radian) + _yOffset;
_radian += _dir * _speed;
if(_radian > 2*Math.PI){
_radian = 0;
}
}
override public function start():void{
_timer.start();
}
override public function stop():void{
_timer.stop();
}
override public function get running():Boolean{
return _timer.running;
}
}
}
所以以直覺來講,RingMotion2D應該要直接沿用CircleMotion2D的定義,簡單的改變每個Object的initial radian就可以達到環繞的效果,所以RingMotion2D理論上是會這樣撰寫:
/* RingMotion2D.as*/
package com.xinyu.motion{
import flash.utils.Timer;
import flash.events.TimerEvent;
import flash.display.DisplayObject;
import com.xinyu.motion.CircleMotion2D;
import com.xinyu.science.Science;
public class RingMotion2D extends Motion{
private var _obj:Array;
private var circleM2D:Array;
public static const CW:int = 1;
public static const CCW:int = -1;
public function RingMotion2D(obj:Array, xOffset:Number = 0 ,yOffset:Number = 0, radius:Number = 100, speed:Number = 0.1, fps:Number = 30, dir:Number = RingMotion2D2.CW){
_obj = obj;
circleM2D = new Array(_obj.length);
for(var i:uint = 0; i < _obj.length; i++){
circleM2D[i] = new CircleMotion2D(_obj[i], xOffset, yOffset, radius, speed, fps, dir, 360 / _obj.length * i);
}
}
override public function start():void{
for(var i:uint = 0; i < _obj.length; i++){
circleM2D[i].start();
}
}
override public function stop():void{
for(var i:uint = 0; i < _obj.length; i++){
circleM2D[i].stop();
}
}
override public function get running():Boolean{
return circleM2D[0].running;
}
}
}
乍看之下,這樣的寫法很簡潔且沒有問題,但是實際去做測試之後問題就會浮現了,那就是效能,一旦物件增多,fps加快,這個Class所用到的timer也會是成比例成長,縱使是再快的CPU也會有些微的影響 (當然你可能會質疑說,誰沒事會在畫面上產生幾百顆球? 但別忘了這個效果可能只是你的project的功能之一,且並不是所有的瀏覽者的電腦都是那麼快的)。
為了降低程式的效能成本,我必須重新定義一個RingMotion,如下:
/* RingMotion2D.as */
package com.xinyu.motion{
import flash.utils.Timer;
import flash.events.TimerEvent;
import flash.display.DisplayObject;
import com.xinyu.science.Science;
public class RingMotion2D extends Motion{
private var _timer:Timer;
private var _radian:Array;
private var _radius:Number;
private var _speed:Number;
private var _dir:int;
private var _xOffset:Number;
private var _yOffset:Number;
private var _obj:Array;
public static const CW:int = 1;
public static const CCW:int = -1;
public function RingMotion2D(obj:Array, xOffset:Number = 0 ,yOffset:Number = 0, radius:Number = 100, speed:Number = 0.1, fps:Number = 30, dir:Number = RingMotion2D.CW){
_obj = obj;
_radian = new Array(_obj.length);
for(var i:uint = 0; i<_obj.length; i++){
_radian[i] = Science.degToRad(360/_obj.length * i);
}
_radius = radius;
_speed = Math.min(1, Math.max(speed, 0.001));
_dir = dir;
_xOffset = xOffset;
_yOffset = yOffset;
_timer = new Timer(1000/fps);
_timer.addEventListener(TimerEvent.TIMER, onTimer);
}
private function onTimer(event:TimerEvent):void{
for(var i:uint = 0; i < _obj.length; i++){
_obj[i].x = _radius * Math.cos(_radian[i]) + _xOffset - _radius;
_obj[i].y = _radius * Math.sin(_radian[i]) + _yOffset;
_radian[i] += _dir * _speed;
if(_radian[i] > 2*Math.PI){
_radian[i] = 0;
}
}
}
override public function start():void{
_timer.start();
}
override public function stop():void{
_timer.stop();
}
override public function get running():Boolean{
return _timer.running;
}
}
}
無可否認這樣的寫法的確違反了OOP的精神,但是為了效能不得如此,因為這樣寫不管有多少個DisplayObject,我們都只需要一個Timer。
看到這裡也許你會發現如果新的RingMotion2D裡只控制一個物件,那不就等於CircleMotion2D,所以理當CircleMotion2D應該要被RingMotion2D給取代掉才對? 未必,畢竟CircleMotion2D在處裡單顆物件上的效能會比RingMotion2D快一點點(也只有那麼微渺的一點點)。
在c++裡我們有virtual function可以用,在Java裡則可以宣告abstract class ,而在actionscript 3.0雖然有undocumented的virtual specifier可以使用,但卻無法達到pure virtual function的效果,所幸還有override可以發揮,我們一樣也可以在actionscript 3.0達到abstract class的效果,一個簡易的範例如下:
/* Motion.as */
package com.xinyu.motion{
public class Motion{
public virtual function get running():Boolean{
return false;
}
public virtual function start():void{
}
public virtual function stop():void{
}
}
}
借用之前motion那一篇文章裡的程式碼,只是這裡我們不使用interface的方式來設定,並且將整個motion獨立出來。可以看到我在Motion裡所宣告的函式都加上virtual關鍵字,實際上有沒有加上都不會有任何差別,只是這麼做是為了一個良好的設計習慣。
底下則是一個SpaceMotion2D繼承於Motion的撰寫方式。
/* SpaceMotion2D */
package com.xinyu.motion{
import flash.display.Sprite;
import flash.utils.Timer;
import flash.events.TimerEvent
import flash.geom.Rectangle;
import flash.display.DisplayObject;
import com.xinyu.motion.Motion;
import com.xinyu.science.Science;
public class SpaceMotion2D extends Motion{
private var _timer:Timer;
protected var _xDir:int;
protected var _yDir:int;
protected var _speed:Number;
protected var _range:Rectangle;
protected var _obj:DisplayObject;
public function SpaceMotion2D(obj:DisplayObject, range:Rectangle, speed:Number = 0.1, fps:Number = 25){
_obj = obj;
_range = range;
_speed = Math.min(20, Math.max(speed, 0.001));
do{
_xDir = Science.randInt(-1,1);
}while(_xDir == 0);
do{
_yDir = Science.randInt(-1,1);
}while(_yDir == 0);
_timer = new Timer(1000/fps);
_timer.addEventListener(TimerEvent.TIMER, onTimer);
}
protected function onTimer(event:TimerEvent):void{
if(_obj.x + _obj.width/2 + _speed > _range.width){
_xDir = -1;
}
if(_obj.x - _obj.width/2 - _speed < _range.x){
_xDir = 1;
}
if(_obj.y + _obj.height/2 + _speed > _range.height){
_yDir = -1;
}
if(_obj.y - _obj.height/2 - _speed < _range.y){
_yDir = 1;
}
_obj.x += _xDir * _speed;
_obj.y += _yDir * _speed;
}
override public function start():void{
_timer.start();
}
override public function stop():void{
_timer.stop();
}
override public function get running():Boolean{
return _timer.running;
}
}
}
如同剛剛所說的,為了培養良好的設計模式,請在start(), stop(), running()前面都加上override以表示這些functions是從Motion繼承而來重新實做的。
所以大致上AS3裡的abstract class就是以這種方式來實現,至於AS4裡會怎麼去定義這個性質就請拭目以待啦!
另外如果你有興趣的話也可以將SpaceMotion2D改成3D,下面就是一個SpaceMotion3D繼承於SpaceMotion2D的寫法:
/* SpaceMotion3D */
package com.xinyu.motion{
import flash.display.Sprite;
import flash.utils.Timer;
import flash.events.TimerEvent;
import flash.geom.Rectangle;
import flash.display.DisplayObject;
import com.xinyu.motion.SpaceMotion2D;
import com.xinyu.science.Science;
public class SpaceMotion3D extends SpaceMotion2D {
private var _zDir:int;
private var _z:Number;
private var _depth:Number;
private var xyScaleRatio:Number;
private var objScaleRatio:Number;
public function SpaceMotion3D(obj:DisplayObject, range:Rectangle, depth:Number, speed:Number = 1, fps:Number=25) {
super(obj, range, speed, fps);
_z = depth;
_depth = depth;
calXYScaleRatio();
calObjScaleRatio();
do {
_zDir = Science.randInt(-1, 1);
} while (_zDir == 0);
}
override protected function onTimer(event:TimerEvent):void {
var zRatio:Number = _z / _depth;
if (_obj.x + _obj.width / 2 + _speed > _range.width - _range.width * xyScaleRatio * zRatio ) {
_xDir=-1;
}
if (_obj.x - _obj.width / 2 - _speed < _range.x + _range.width * xyScaleRatio * zRatio) {
_xDir=1;
}
if (_obj.y + _obj.height / 2 + _speed > _range.height - _range.height * xyScaleRatio * zRatio ) {
_yDir=-1;
}
if (_obj.y - _obj.height / 2 - _speed < _range.y + _range.height * xyScaleRatio * zRatio) {
_yDir=1;
}
if (_z - _speed < 0) {
_zDir = 1;
}else if (_z + _speed > _depth) {
_zDir = -1;
}
_obj.x += _xDir * _speed;
_obj.y += _yDir * _speed;
_z += _zDir * _speed;
_obj.scaleX = 1 - objScaleRatio * zRatio;
_obj.scaleY = 1 - objScaleRatio * zRatio;
}
private function calXYScaleRatio():void{
var max: Number = Science.findMax(_range.width, _range.height);
xyScaleRatio = _depth / ( 4*max);
xyScaleRatio = Math.min(0.9, Math.max(xyScaleRatio, 0.1));
}
private function calObjScaleRatio():void{
var min: Number = Science.findMin(_range.width, _range.height);
objScaleRatio = min / (2 * _depth);
objScaleRatio = Math.min(0.9, Math.max(objScaleRatio, 0.1));
}
}
}
這裡的3D效果是我簡單以視覺的判斷產生出來的,並沒有透過嚴謹的mathematical計算來達成,也就是說SpaceMotion3D.as並不是仰賴Flash CS4的z-axes的功能,想要更完善的功能請使用PaperVision3D吧!
做個小小的紀錄:
另外還有一篇Tips for learning ActionScript 3.0,值得初學者去讀。
Download: MotionUIComponent.zip
平常在Flash底下辛苦寫的actionscript project如果要移植到Flex Project上可以透過UIComponent來承接Sprite,只是有一個限制就是如果你有使用 fl.* package在Flex裡是不支援的。這裡我直接以[Flash] Motion為範例,在這一篇裡我們寫了不小的程式碼來達到多種Motion的特效,如果要拿到Flex Project上當然是不希望要重新撰寫,以下就是一個移植的範例程式碼:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" applicationComplete="initApp()" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#7E9ED7, #CFE6EF]" width="400" height="300" viewSourceURL="srcview/index.html">
<mx:Script>
<![CDATA[
import flash.display.Sprite;
import xinyu.geom.Ball;
import xinyu.science.Science;
import xinyu.motions.SpaceMotion2D;
import xinyu.motions.CircleMotion2D;
private var ball:Ball;
private var ballMask:Sprite;
private function initApp(): void {
ball = new Ball(0xCCCCCC, 0x000055, 10, -45);
rootStage.addChild(ball);
ball.setMotion(new SpaceMotion2D(new Rectangle(0,0,rootStage.width, rootStage.height), 5, 30));
ball.x = Science.randInt(0, rootStage.width);
ball.y = Science.randInt(0, rootStage.height);
motionPanel.title = "2D Space Motion";
initMask();
}
private function initMask():void{
ballMask = new Sprite();
ballMask.graphics.beginFill(0x000000);
ballMask.graphics.drawRect(0,0,rootStage.width, rootStage.height);
ballMask.graphics.endFill();
rootStage.addChild(ballMask);
rootStage.mask = ballMask;
}
private function run():void{
ball.motion.run();
}
private function stop():void{
ball.motion.stop();
}
private function onMotionClicked(event:MouseEvent):void{
var running:Boolean = ball.motion.running;
switch(event.target.label){
case "2D Space Motion":
ball.setMotion(new SpaceMotion2D(new Rectangle(0,0,rootStage.width, rootStage.height), 5, 30));
break;
case "2D Circle Motion":
ball.setMotion(new CircleMotion2D(ball.x, ball.y, 50, 0.1, 30, CircleMotion2D.CW));
break;
}
motionPanel.title = event.target.label;
if(running){
run();
}
}
]]>
</mx:Script>
<mx:VBox verticalGap="4" left="10" top="10" right="10" bottom="10">
<mx:Panel id="motionPanel" width="100%" height="240" layout="absolute" title="2D Space Motion" horizontalAlign="center" verticalAlign="middle" cornerRadius="5">
<mx:UIComponent id="rootStage" width="100%" height="100%" x="0"/>
</mx:Panel>
<mx:ApplicationControlBar width="100%" height="100%">
<mx:Button label="Run" click="run()" width="100%" height="100%"/>
<mx:Button label="Stop" click="stop()" width="100%" height="100%"/>
<mx:Button label="2D Space Motion" click="onMotionClicked(event)" width="100%" height="100%"/>
<mx:Button label="2D Circle Motion" click="onMotionClicked(event)" width="100%" height="100%"/>
</mx:ApplicationControlBar>
</mx:VBox>
</mx:Application>
從Flex Reference裡可以得知UIComponent是FlexSprite的子類別,而FlexSprite繼承於Sprite,所以當我們要加入當初在Flash底下設計的DisplayObject時,要在Flex主場景裡新增一個UIComponent來包住Ball物件。
從這樣的特性可以得知,以後如果要直接在Flex撰寫ActionScript可以直接繼承於UIComponent即可。
在Flex SDK裡編寫的Socket程式在連線的過程中出現下面的訊息:
這是Flash Player 安全原則設定,在做網站連線或者Socket連結時需要先取得cross domain policy才能繼續連線。
以Socket Policy File為例,crossdomain.xml寫法如下:
<policy-file-request/>
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM
"http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="yoursockethosts.com" to-ports="2115"/>
</cross-domain-policy>
下面這個則為URL Policy File的寫法:
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM
"http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<site-control permitted-cross-domain-policies="master-only"/>
<allow-access-from domain="www.yourwebpage.com"/>
</cross-domain-policy>
在flash裡要要載入crossdomain最簡易的方法就是使用loadPolicyFile(),如下:
package{
import flash.net.Socket;
import flash.events.Event;
import flash.events.ProgressEvent;
import flash.text.TextField;
import flash.display.Sprite;
import flash.system.Security;
public class SocketDemo extends Sprite{
private var socket:Socket;
private var textField:TextField;
Security.loadPolicyFile("http://localhost/crossdomain.xml");
public function SocketDemo(){
socket = new Socket("localhost", 2115);
socket.addEventListener(Event.CONNECT, onConnected);
textField = new TextField();
addChild(textField);
}
private function onConnected(event:Event):void{
textField.text = "connected!";
}
}
}
除了透過從 flash裡面主動去載入crossdomain.xml之外,你也可以把policy file寫入到server然後由server傳到Flash Socket裡。下面的寫法是承襲[Flash] Flash Socket & C++/Java Socket
這一篇裡的C++ Socket的部份。
string xml = "<cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"1025-9999\"/></cross-domain-policy>";
client.send(xml);
詳細的cross domain policy file的資料可以參考下面的網頁:
Cross-domain policy file specification
如果你要了解更多關於Flash Security的問題請參考下面的網頁:
Download: Reminder.zip
在比較複雜一點的開發專案裡就比較常用到Event Dispatching,因為並不是所有的物件都可以加到Timeline上(未必繼承於Sprite),這時dispatchEvent就派上用場。
舉個例子來說,假設我們今天要設計一個Reminder,當Reminder時間一到就顯示當初設定的訊息。比較笨的做法就是不斷的去測試Reminder的物件裡的時間是否已經Timeout,然後再顯示設定的訊息,但我想應該沒有人會這樣做。因為在Reminder裡面是使用Timer來計算時間,所以就直接使用TimerEvent.COMPLETE,當Timer結束時在主場景上顯示設定的訊息,要達到這個條件 ,Reminder必須跟主要顯示物件是繼承或者是parent/child的關係,如此才能控制parent的functions以及properties。或者你也可以在Reminder裡面新增TextField之類的物件,只是這樣的話顯示訊息的UI就被鎖死在Reminder裡,所以這種方式還是不夠好,為了讓Reminder就是專心做計時並且回傳當初設定的訊息,下面就是使用dispatchEvent來實做的方法。
/* Reminder */
package{
import flash.events.TimerEvent;
import flash.events.Event;
import flash.utils.Timer;
import flash.events.EventDispatcher;
public class Reminder extends EventDispatcher{
private var timer:Timer;
private var msg:String;
public function Reminder(minsec:uint, msg:String){
this.msg = msg;
timer = new Timer(minsec,1);
timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimerComplete);
timer.start();
}
public function get message():String{
return msg;
}
private function onTimerComplete(event:TimerEvent):void{
dispatchEvent(new Event("onTimeOut"));
}
}
}
這個範例裡設定的dispatchEvent名稱為onTimeOut,在ReminderDemo裡面要用到。(如果你想讓Reminder裡顯示一些資訊,可以將原本繼承於EventDispatcher改為Sprite,如此在ReminderDemo裡就可以將reminder加入為child。(因為ReminderDemo是繼承於Sprite),但是如前面所提,我們不希望Reminder本身做那麼多事情。)
/* ReminderDemo */
package{
import flash.display.Sprite;
import flash.events.Event;
import Reminder;
public class ReminderDemo extends Sprite{
private var reminder:Reminder;
public function ReminderDemo(){
reminder = new Reminder(1000," event dispatched by remainder! ");
reminder.addEventListener("onTimeOut", onTimeOut);
}
private function onTimeOut(event:Event):void{
trace(event.target.message);
}
}
}
Reminder物件經過dispatchEvent之後就多了一個onTimeOut可以設定,我刻意在Reminder裡面將msg設定為read-only,如此我們就可以接收到當初設定的訊息(假使你要一次開啟很多的Reminder時,這樣的寫法應該會比較便捷)。
有了Event Dispatching的功能之後,在OOP裡會更方便許多。
P.S. 如果你熟悉flash.utils這個package,你應該會發現有一個叫做setTimeout可以用,但是在AS3裡,Adobe建議我們使用Timer來完成TimeOut的功能(我想他們可能很想把setInterval/setTimeout給拿掉。)
Installation pip install orange3 Run orange python -m Orange.canvas