2012-11-06

ActionScript3.0と三角関数(sin,cos)(初歩)

[話者] 数学のグラフはこういう座標系だよな。

Flashの座標系はこうなんだ。

https://sites.google.com/site/itouhiro/2012/20121105sincos.swf

[合いの手] Y軸の+-が逆だね。これはFlashが特殊なの?

[話者] コンピュータのグラフィック関連はほぼ全部Flashと同じだよ。Windowsプログラミングも、TVゲーム機も、左上 ┌ が原点(0,0)の座標系を使ってる。

[合いの手] なぜ、こんなことに?

[話者] それは文字を横書きで書くとき、最初の行を1行目、次の行を2行目と呼ぶだろう? ほら、Yが下に行くほど行数はアップするじゃないか。

コンピュータのグラフィックも同じように、左上←↑から描きはじめる。文字と同じように、横一列書き終わったら、次の行に移動して描く‥‥というふうに描かれる。これはアナログテレビのブラウン管の時代からそうなってる。

[合いの手] で、三角関数はそれとどう関係するのさ?

[話者] 数学のグラフで30度の角度といえば、こうだよな。

Flashで30度の角度といえば、こうなる。

https://sites.google.com/site/itouhiro/2012/20121105sincos2.swf

[合いの手] えー。角度も 上下が逆になるのか?

[話者] というのも、Wikipedia:三角関数 の定義から、以下がなりたつ。

sinθ は、半径 1.0 中心(0,0)の正円(単位円)の y座標に一致する。 ここでθ(シータ)はx軸と「動点と原点を結ぶ線分」のなす角度。

[合いの手] つまり、

sin 0°=0
sin 90°=1

と決まっている。

  1. 定義から sinθ = y といえるので、 90°のときの y座標(=sin 90°)は 1 。

  2. Flashで(0,1)は、X軸より下側にある。

だから、角度も、数学とは逆になってしまうわけだね。

[話者] それでは、このFlashのソースコードを見てみよう。

2つのasファイルから構成されている。ひとつめのAxisクラスは、ここでは「30°」とかの数値を表示するために使っている。

Axis.as

package  
{
    import flash.display.Sprite;
    import flash.text.TextField;
    import flash.text.TextFieldAutoSize;
    
    /**
     * ...
     * @author itouhiro
     */
    public class Axis extends Sprite 
    {
        private var tfd:TextField;
        
        public function Axis(str:String = '') 
        {
            tfd = new TextField();
            tfd.text = str;
            tfd.autoSize = TextFieldAutoSize.LEFT;
            addChild(tfd);
        }
        
        public function print(str:String = ''):void 
        {
            tfd.text = str;
        }
    }
}

[合いの手] これはたいして難しくない。print()メソッドで文字列を表示させてるだけだね。

[話者] 次のがメインだ。

Main.as

package 
{
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.text.TextField;
    
    /**
     * ...
     * @author itouhiro
     */
    [SWF(backgroundColor="0xf8f8f8", width="320",height="240", frameRate="10")]
    public class Main extends Sprite 
    {
        private var point:Sprite;
        private var theta:Number;
        private var axis:Axis;
        private var line:Sprite;
        
        public function Main():void 
        {
            if (stage) init();
            else addEventListener(Event.ADDED_TO_STAGE, init);
        }
        
        private function init(e:Event = null):void 
        {
            removeEventListener(Event.ADDED_TO_STAGE, init);
            // entry point
            drawGrid();
            
            point = new Sprite();
            addChild(point);
            
            line = new Sprite();
            addChild(line);
            
            theta = 0;
            addEventListener(Event.ENTER_FRAME, enterFrameHandler);
            
            axis = new Axis();
            axis.x = 200; axis.y = 150;
            addChild(axis);
        }
        
        private function getRamdomColor():int 
        {
            var color:int;
            for (var i:int = 0; i < 3; i++) {
                var c:int = Math.random() * 0xcc;
                color = color << 8 | c;
            }
            return color;
        }
        
        private function enterFrameHandler(e:Event):void 
        {
            if (theta === 0)
            {
                point.graphics.lineStyle(1, getRamdomColor());
                point.graphics.drawCircle(0, 0, 2);
            }
            //動点表示
            point.x = Math.cos(theta) * 100 + (stage.stageWidth / 2);
            point.y = Math.sin(theta) * 100 + (stage.stageHeight / 2);
            //度数表示
            axis.print(int(theta * 180 / Math.PI) + "°");
            //動点と原点を結ぶ線分 を表示
            line.graphics.clear();
            line.graphics.lineStyle(1, 0x333333);
            line.graphics.moveTo(stage.stageWidth / 2, stage.stageHeight / 2);
            line.graphics.lineTo(point.x, point.y);
            
            theta += (Math.PI / 2) / 90;
            if (theta > Math.PI / 2)
            {
                theta = 0;
            }
        }
        
        private function drawGrid():void 
        {
            var i:int;
            
            //グリッド (方眼紙)
            graphics.lineStyle(1, 0xeeeeee);
            for (i = 0; i < stage.stageWidth; i += 20)
            {
                graphics.moveTo(i, 0);
                graphics.lineTo(i, stage.stageHeight);
            }
            for (i = 0; i < stage.stageHeight; i += 20)
            {
                graphics.moveTo(0,i);
                graphics.lineTo(stage.stageWidth,i);
            }
            
            // Y軸
            graphics.lineStyle(1, 0x333333);
            graphics.moveTo(stage.stageWidth / 2, 0);
            graphics.lineTo(stage.stageWidth / 2, stage.stageHeight);
            // X軸
            graphics.moveTo(0, stage.stageHeight / 2);
            graphics.lineTo(stage.stageWidth, stage.stageHeight / 2);
            
            // 文字
            var text_x:TextField = new TextField();
            text_x.text = 'x';
            text_x.x = stage.stageWidth - 10;
            text_x.y = stage.stageHeight / 2;
            addChild(text_x);

            var text_y:TextField = new TextField();
            text_y.text = 'y';
            text_y.x = stage.stageWidth / 2 - 10;
            text_y.y = 0;
            addChild(text_y);
            
            var text_90degree:TextField = new TextField();
            text_90degree.text = '(0, 1)';
            text_90degree.x = Math.cos(Math.PI/2) * 100 + (stage.stageWidth / 2);
            text_90degree.y = Math.sin(Math.PI/2) * 100 + (stage.stageHeight / 2) - 10;
            addChild(text_90degree);
        }
    }
}

[合いの手] function getRamdomColorは何してるの?

[話者] これは三角関数とは関係ないんだけど、ランダムな色を得る関数だ。上のフラッシュをよく見ると、動く点の色が1回ごとに変わっているんだよ。

色は24bitの符号なし整数 uint (unsigned integerの略)としてFlashで指定できる。0x12AB34 という形式は見たことあるだろう。RGB という色の表し方も聞いたことがあると思う。

  • Red に 0x00~0xff (10進数では0~255)
  • Green に 0x00~0xff
  • Blue に 0x00~0xff

の各色8bitを指定して、合計24bitの色になるのだ。

そのRGBの各色をランダムに選択しているのが、以下の2行目。

            for (var i:int = 0; i < 3; i++) {
                var c:int = Math.random() * 0xcc;
                color = color << 8 | c;
            }

Math.random()0 <= n < 1 の小数を返す。0.6とかが返ってくるんだ。それカケる0xcc (10進数では204)で、0 <= n < 0xcc の値になる。ここで 0xff (10進数では255)ではなく 0xccを使っている理由は、あまり明るい色だと背景色0xFFFFFFとまぎれて見つけにくくなるから。ある程度暗くて背景とはっきり区別のつく色がほしくて 0xcc を使っている。

color = color << 8はビット演算。2進数で表すと、

color      = 0x 00000000 00000000 00111111
color << 8 = 0x 00000000 00111111 00000000

のように←に8bitぶん桁上がり(シフト)する。

color = color | cの縦棒は or演算子。

color     = 0x 00000000 00111111 00000000
c         = 0x 00000000 00000000 00110011

の場合、or演算子は「どちらかが1なら結果も1」

1 or 1 -> 1
1 or 0 -> 1
0 or 0 -> 0

だから、

color | c = 0x 00000000 00111111 00110011

という結果になる。ちなみに & (and演算子)は「どちらも1の場合だけ1、それ以外は0」。

[合いの手] よく見れば‥‥動点の色 変わってるね。めだたないけど

[話者] 三角関数の話に戻ると、

            if (theta > Math.PI / 2)
            {
                theta = 0;
            }

ここの判定で、角度θ(シータ)が90°を越えたら0°に戻している。Math.PI / 2が90°を表すんだ。

[合いの手] Math.PI ってパイ π つまり 3.14だよね。

[話者] そう。変数thetaはラジアン radian という単位の値をもつ。

90°という度数表示は degree ディグリーといって、ラジアンとは別物だ。

[合いの手] degree と radian は別物だけど、どちらも角度を表すんだね

[話者] 対比すると、こう。

 90 degree = π/2 radian    90°は2分の1パイラジアン(1.57ラジアン)
180 degree = π radian     180°は1πラジアン(3.14ラジアン)
270 degree = 3/2π radian  270°は2分の3πラジアン
360 degree = 2π radian    360°は2πラジアン(6.28ラジアン)

https://sites.google.com/site/itouhiro/2012/20121111sincos5.swf

[合いの手] radianは扱いにくい感じがする。小数だし。

[話者] しかし Math.sin()とか Math.cos()に与えられる値はラジアンだけだ。

ラジアンは以下のコードでdegreeに変換できる。

degree = radian * 180 / Math.PI

ソースコードだとint(theta * 180 / Math.PI)で度数に変換していた。変換したdegreeは小数なことが多いので、int()で小数点を切り捨てて整数にしている。

[合いの手] このソースコードは結局、変数thetaに「角度」を入れて、「座標」を求めたわけだね。

逆に、座標がわかってるとき、角度を求めるのはどうするのかな

[話者] サンプルを作ってみた。

実際のFlash。

https://sites.google.com/site/itouhiro/2012/20121105sincos3.swf

[合いの手] おおー。当たり判定はないのか。

[話者] ないよ。 それではソースを見てみよう。

3つのソースコードがある。Mainクラスは自機と敵機を動かし、敵の弾丸発射も受け持つ。

Main.as

package 
{
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    
    /**
     * ...
     * @author itouhiro
     */
    [SWF(backgroundColor="0xf8f8f8", width="320",height="240", frameRate="15")]
    public class Main extends Sprite 
    {
        private var myShip:SpaceShip;
        private var enemyShip:SpaceShip;
        private var bullets:Array;
        
        public function Main():void 
        {
            if (stage) init();
            else addEventListener(Event.ADDED_TO_STAGE, init);
        }
        
        private function init(e:Event = null):void 
        {
            removeEventListener(Event.ADDED_TO_STAGE, init);
            // entry point
            myShip = new SpaceShip(stage.stageWidth / 3 * 2, stage.stageHeight / 3 * 2, 0x9999CC, 0xFFFFFF);
            addChild(myShip);
            myShip.addEventListener(Event.ENTER_FRAME, myShipEnterFrameHandler);
            
            bullets = [];
            
            enemyShip = new SpaceShip(stage.stageWidth / 2, stage.stageHeight / 2, 0xCC9999, 0xFF6666);
            addChild(enemyShip);
            enemyShip.addEventListener(Event.ENTER_FRAME, enemyShipEnterFrameHandler);
        }
        
        private function myShipEnterFrameHandler(e:Event):void 
        {
            var dx:Number = mouseX - myShip.x;
            var dy:Number = mouseY - myShip.y;
            myShip.x += dx / 10;
            myShip.y += dy / 10;
        }
        
        private function enemyShipEnterFrameHandler(e:Event):void 
        {
            var dx:Number = mouseX - enemyShip.x;
            var dy:Number = mouseY - enemyShip.y;
            var radian:Number = Math.atan2(dy, dx);
            enemyShip.rotation = radian * 180 / Math.PI;
            
            if (Math.random() * 10 > 8.5)
            {
                for (var i:int = 0; i < bullets.length; i++) {
                    if ( ! bullets[i].active)
                    {
                        removeChild(bullets[i]);
                        delete bullets[i];
                        bullets.splice(i, 1);
                    }
                }
                var newBullet:Bullet = new Bullet(enemyShip.x, enemyShip.y, enemyShip.rotation);
                bullets.push(newBullet);
                addChild(newBullet);
            }
        }
    }
}

function enemyShipEnterFrameHandlerの中身を見てみよう。ここでまさに、「座標」がわかっているときに「角度」を割り出す、ということをしている。

[合いの手] radian:Number = Math.atan2(dy, dx)というところで、YとXの線分の長さから角度を割り出してるみたいだ。サイン sin やコサイン cos は習ったけど、atan2()というのは習ってないなあ

[話者] サイン・コサイン・タンジェントというのは習ったと思うが、タンジェント Math.tan()は「角度」を与えると「対辺÷隣辺の比率」を返す。その逆に「比率」を与えると角度を返すアークタンジェント Math.atan() というのがあるんだ。

しかし Math.atan() には第一象限と第三象限、第二象限と第四象限の値が同じになってしまうという欠点があり、その欠点をなくしたのが atan2()なんだって。

[合いの手] はあー。まあ動けばいいけどさ。確かに画面中央にある敵機は、回転して常に自機の方向に向くね。

[話者] enemyShip.rotation = radian * 180 / Math.PIというのは、rotationプロパティはラジアンじゃなくて度数(degree)しか受け付けないので、ラジアンをdegreeに変換している。

[合いの手] 敵の弾射出タイミングが一定じゃないのは?

[話者] if (Math.random() * 10 > 8.5)という箇所で判定している。Math.random()*10とすると、0.0~9.999‥‥ までの小数を返す。10.0は返さないぞ。で、そのうちの 8.5より上の場合だけ弾を射出するようにしてる。つまり、20ぶんの3(10ぶんの1.5を倍にした)の割合で弾を射出して、このFlashのフレームレートはframeRate="15"とあるので1秒15コマだから‥‥ 1秒に発射される弾の数は平均すれば、20ぶんの45発、つまり2.25発くらいになるな。

[合いの手] この8.5を5.0などにすれば、もっと弾を発射することになるわけだね。 自機の動きはマウスを追いかけてるけど?

[話者] それはfunction myShipEnterFrameHandlerの中でやってる。

            myShip.x += dx;
            myShip.y += dy;

とすれば、自機はマウスカーソルにぴったりくっつくわけだが、それではおもしろくないので多少の不自由さを導入する。

            myShip.x += dx / 10;
            myShip.y += dy / 10;

と、離れた距離の10ぶんの1だけ近づくことにすれば、マウスカーソルに徐々に近づくのを表現できる。

[合いの手] これって10回実行すればマウスカーソルに到達するはずだよね。10フレーム、つまり1秒以下でマウスカーソルの真下にくるべきなのに、もっと時間がかかってるようなんだけど‥‥

[話者] いや、それは違うよ。毎回dx,dyを計算し直してるから。自機がマウスカーソルに近づくほど「離れた距離の10ぶんの1」も小さくなるから、つまり、自機はマウスカーソルの近くにきたら速度が遅くなるんだよ。

[合いの手] ああ、そんな感じ。

[話者] 次のソースコード、SpaceShipクラスは単に自機と敵機のかたちを描いてるだけだね。ポイントは、0°の方向、つまり右側に矢印先端を向けておく、ということ。そうすれば、回転角度をそのまま与えられるからね。

SpaceShip.as

package  
{
    import flash.display.Sprite;
    
    /**
     * ...
     * @author itouhiro
     */
    public class SpaceShip extends Sprite 
    {
        public function SpaceShip(_x:int, _y:int, colorOutline:uint, colorFill:uint) 
        {
            x = _x; y = _y;
            drawAirFighter(colorOutline, colorFill);
        }
        
        private function drawAirFighter(colorOutline:uint, colorFill:uint):void 
        {
            graphics.lineStyle(2, colorOutline);
            graphics.beginFill(colorFill);
            //graphics.drawRect(-10, -10, 20, 20);
            graphics.moveTo(-10, -10);
            graphics.lineTo(-10, 10);
            graphics.lineTo(25, 0);
            graphics.lineTo(-10, -10);
            graphics.endFill();
        }
    }
}

次のソースコード Bulletクラスは敵弾を描いて、敵弾の動きも制御する。そして敵弾の消去判定もおこなう。

Bullet.as

package  
{
    import flash.display.Sprite;
    import flash.events.Event;
    
    /**
     * ...
     * @author itouhiro
     */
    public class Bullet extends Sprite 
    {
        private var radian:Number;
        public var active:Boolean;
        
        public function Bullet(_x:int, _y:int, _direction:Number) 
        {
            x = _x; y = _y;
            radian = _direction * Math.PI / 180;
            drawBullet();
            active = true;
            addEventListener(Event.ENTER_FRAME, bulletEnterFrameHandler);
        }
        
        private function bulletEnterFrameHandler(e:Event):void 
        {
            x += Math.cos(radian) * 5;
            y += Math.sin(radian) * 5;
            if (x < 0 || x > stage.stageWidth || y < 0 || y > stage.stageHeight)
            {
                removeEventListener(Event.ENTER_FRAME, bulletEnterFrameHandler);
                active = false;
            }
        }
        
        private function drawBullet():void 
        {
            graphics.lineStyle(2, 0x000000);
            graphics.beginFill(getRandomColor(0x33,0xff));
            graphics.drawCircle(0, 0, 4);
            graphics.endFill();
        }
        
        private function getRandomColor(min:uint, max:uint):uint 
        {
            return (min + Math.random() * (max-min)) << 16 | (min + Math.random() * (max-min)) << 8 | (min + Math.random() * (max-min));
        }
    }
}

[合いの手] 敵弾ってカラフルだよね。

[話者] function getRandomColorは先ほどのより機能追加して、最低値と最高値を指定できるようにした。

[合いの手] 敵弾の動きはx += Math.cos(radian) * 5;なのか。コサインがこんなふうに使われるとは数学の授業で教えてもらってない

ところで画面端からはみ出て消えるはずの敵弾が、なぜか少し画面端に残るんだけど?

[話者] ここでは解決しないで残してある。

毎フレーム、敵弾消去チェックする 画面端判定に敵弾直径も足す とかすれば解決するんだけどね。

処理の流れとしては、弾オブジェクト自身が画面端に来たか判定する。画面端を越えたら active=off。 Mainオブジェクトで新弾を発射するときに、旧弾もチェックして、active==offだったらオブジェクトを消す。

敵弾オブジェクトはnew Bullet()で生成してるわけだけど、オブジェクトを消すには、 removeChild()で画面に表示しなくして、どの変数からもそのオブジェクトへの参照をなくせば、そのうちメモリからも消えるらしい。 参考: http://f-site.org/articles/2010/05/09072731.html

弾オブジェクトに自身をremoveChildさせる方法はないんだろうか‥‥? → 参考: http://stackoverflow.com/questions/3184623/how-to-make-a-movieclip-remove-itself-in-as3

delete を使うのは意味があるのか不明。 参考: http://fladdict.net/blog/2006/08/as3_9.html

[合いの手] 当たり判定つければシューティングゲームになりそうだな。

0 件のコメント:

コメントを投稿

人気記事