衝突判定

衝突判定の仕組み

キャラクターとブロックの衝突判定の仕組みについて解説します。また、サンプルでは少々特殊な衝突方法を採用しているため、その方法も併せて解説します。キャラクターを地面に立たせたり、ブロックに飛び乗ったりといった動作を行うには、キャラクターと障害物の衝突を判定する必要があります。衝突判定を行う方法の一つに、hitTestObjectメソッドを使う方法があります。このメソッドを利用すれば2つの表示オブジェクトを調べて重なっているのか調べることができます。戻り値として、2つが重なっていれば"TRUE"、重なっていなければ"FALSE"が返ってきます。

hitTestObject

しかし、この方法は手軽に衝突判定を行うことができる一方で、処理負荷が高くなりやすいという問題があります。

マップチップ

そこで、衝突判定の負荷を極力減らすために、本サンプルではマトリクスを使って衝突判定を行う方法を採用します。マトリクスによる衝突判定とは、何もない領域を”0”、ブロックがある領域を”1”とし、その領域が0か1かで調べることで衝突を判定する考え方です。この方法を使えば、全てのフレームで、全てのブロックとの衝突判定するのではなく、キャラクター周辺の座標が0か1かを調べるだけで済み、ブロックが増えても計算量は常に一定になるので、hitTestObjectを使うよりも無駄なく高速に衝突判定を行うことができるようになります。

衝突判定マトリクスの実装

衝突判定マトリクスの定義はArrayクラスを使っています。例えば、縦5×横10のマトリクスを定義する場合には以下のように記述します。

matrix:Array = [
    [0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0,0,0],
    [1,1,1,1,1,1,1,1,1,1]
];

このマトリクスで実際に衝突判定をするためには、先述の図で示し多様にコンテンツを方眼紙のように区切り、マトリクス上の座標と、コンテンツ上の座標を対応させる必要があります。例えば、20px四方で区切った場合、コンテンツ上のx座標が0~19の場合のマトリクスのx座標は"0"、20~39の時は”1”といった具合です。サンプルでは、このようなマトリクス座標への変換を簡単に行うために、以下の式を用いています。

var X:uint = Math.floor(x / 区切りの大きさ(20));
var Y:uint = Math.floor(y / 区切りの大きさ(20));

この式を用いて得られたマトリクス座標を使えば、以下のように記述することでマトリクス座標上の数値に簡単にアクセスできるようになります。

matrix[Y][X]

また、衝突判定マトリクスは、キャラクターやアイテム、敵キャラクターなど、衝突判定を必要とするあらゆるモノから参照されることになるので、クラスとして定義するなどして、どこからでも参照しやすい方法で定義しておくとよいでしょう。

キャラクターの操作

キャラクターの操作や移動アニメーションの実装方法と、その中で衝突判定がどのように組み込まれているのかを解説します。キャラクターの移動やアニメーションは、毎フレームXとYの値を変えてあげることで実装できます。 たとえば、毎フレームX方向に10px、Y方向に20px動かしたい場合は以下のように書きます。

//イベントリスナの登録
addEventListener(Event.EnterFrame,_onEnterFrame);
//毎フレーム実行されるメソッド
function _onEnterFrame(event:Event):void{
    x += 10;
    y += 20;
}

表示されているオブジェクトを動かすという処理は、このように毎フレームオブジェクトのX座標とY座標を変化させることで表現しています。ということは、キーボードの操作によって毎フレームの座標の変化量(移動量)を変えることができれば、キャラクターを操作できるということになるわけです。これらの処理を簡単にまとめると、以下のような流れになります。

【キーボード操作の取得】→【移動量の計算】 → 【移動先への衝突判定と移動】

上記の3つの処理を実装することで、キーボードによる基本的な操作は可能になります。 それではまず、「キーボードの操作の取得」からみていくことにしましょう。

キーボード操作の取得

キャラクターをキーボード操作するには、キーボード操作を取得する必要があります。キーボード操作を取得するためには、ユーザーがキーを押したという行動を、Animate側で取得しなければなりません。このような場合に使われるのが、addEventListener(アッドイベントリスナー)メソッドです。 Animateでは、ユーザーの様々な行動やプログラムの挙動を「イベント」として扱います。このメソッドは、そのイベントが発生したときに実行される関数を登録することができます。キーボード操作を取得するには「KeyboardEvent」というクラスを使います。例えば、ステージで左右のキーが押されたらonKeyDown関数を実行し、どちらのキーが押されたかを判別して処理を変えたい場合には、以下のように記述します。(キーボードのイベントを取得するには、キーボードが半角入力になっている必要があります。)

import flash.events.KeyboardEvent;
import flash.ui.Keyboard;
this.stage.addEventListener(KeyboardEvent.Key_DOWN,_onKeyDown);

function _onKeyDown(event.keyCode){
    switch(event.keyCode){
        case Keyboard.LEFT:
        //左キーが押された時の処理をここに記述する
        break;
        case Keyboard.RIGHT:
        //右キーが押された時の処理をここに記述する
        break;
    } //switch(event.keyCode)
} //function _onKeyDown

KeyboardEventのイベント リスナー_onKeyDownでは、event.keyCodeで押されたキーコードを取得することができるため、そのコードを判別して処理を分岐させることができます。キーボードにはそれぞれのキーボードにはそれぞれのキー対応する値が定義されています。例えば、Shiftキーならば16、スペースキーならば32といった具合です。しかし、いちいちキーコードを覚えるのは大変ですね。幸いにも特によく使われるキーコードは、Keybaordクラスの定数として定義されているので安心してください。前述のコードでいうKeyboard.SPACEといった部分がそうです。

Keyboard.ENTER 13
Keyboard.LEFT 37
Keyboard.SHIFT 16
Keyboard.UP 38
Keyboard.CONTROL 17
Keyboard.RIGHT 39
Keyboard.SPACE 32
Keyboard.DOWN 40
移動量の計算

移動量とは、毎フレームキャラクターが動く距離のことです。移動量は、左右キーによる移動(X方向の移動量)と、ジャンプキーと重力による移動(Y方向の移動量)の2種類があります。これらの値は、_xVector、_yVectorとして定義してあります。これ以外にもいくつか必要なプロパティがあるので、まずはそこを確認しましょう。
移動量の計算に必要なプロパティ
移動量の計算に必要なプロパティは全部で以下の5つです。

var _xVector:Number = 0;
var _yVector:Number = 0;
//キーが押されているのかの状態を保持するプロパティを定義する
var _isLeftKeyDown:Boolean;
var _isRightKeyDown:Boolean;
//ジャンプ状態を保持するプロパティを定義する
var _isJumping:Boolean;

_isLeftKeyDown、_isRightKeyDownは、左右のキーが押されているかを保持する変数です。サンプルで、左右のキーを押し続けると移動速度が早くなるのは、これらの変数を使い、キーの状態によって移動量を増減させているためです。 _isJumpingは、現在キャラクターがジャンプ中であるかを保持する変数です。ジャンプ中や落下中はジャンプすることができないようにこの変数で状態を保持しています。これらの変数を、キーボード操作やキャラクターの状態で変化させ、全体の動作の整合性をとっています。次はキーボード操作まわりの処理を見ていきます。
キーボード操作発生時の処理
キーボードが押された際の処理は、基本的にキーの状態の保持と移動量の補正、そしてジャンプ処理の3つです。 移動量の補正とは、例えば、左キーが押された時に、_xVectorが右に大きな値を取っ手いたら、一定値以下に減速してあげるような処理のことです。これは、キャラクターが方向転換するのを円滑に行うために必要な処理になります。


//キーボードが押された際の処理
function _onKeyDown(event:KeyboardEvent):void{
    switch(event.keyCode){
        case Keyboard.SPACE:
            jump();
            break;
        case Keyboard.LEFT:
        //_xVectorを一定値に補正する
        if(8 < _xVector){
            _xVector = 8
        }else if(_xVector < 2 && 0 < _xVector){
            _xVector = 0;        
        }
        _isLeftKeyDown = true;
        break;
        case Keyboard.RIGHT:
        //_xVectorを一定値に補正する
        if(_xVector < -8){
            _xVector = -8
        }else if(-2 < _xVector && _xVector < 0){
            _xVector = 0;        
        }
        _isRightKeyDown = true;
        break;
    }//switch(event.keyCode)
}//function _onKeyDown

ジャンプ処理は、_yVectorをマイナス方向に変更してあげる処理です。また、空中でジャンプができないように、_yVectorの値と_isJumpingのフラグで、ジャンプ処理が実行されるタイミングを限定しています。

//ジャンプ操作時の処理
function jump(jumpPower:Number=20):void{
    //現在地上に立っていない場合(ジャンプ中)は処理を行わない
    if(_yVector != 0 || _isJumping = true){
        return;
    }
    //_yVectorをジャンプパワー分だけ上に向ける
    _yVector = -20;
    //_isJumpingを"TRUE"にしてジャンプ中であることを保持する
    _isJumping = true;
}

キーボード操作発生時の処理はこれだけです。移動量の計算のメインとなる処理は、どちらかというと毎フレームの処理にあります。

毎フレームの処理

毎フレームの主な処理は、x方向とy方向の移動量の計算と補正です。x方向の移動量は、左右のキーのどちらかが押されているときは値を増減させ、押されていない場合は0に近づけます。これは、キャラクターの動きを減速させ自然に停止させるためです。y方向の移動量は、基本的に重力を加算するのみです。

//x方向の移動量を計算
function _calcXVector():void{
    //左右どちらのキーが押されていた場合、x方向の力を変更する
    if(_isLeftKeyDown == true){
    _xVector -= 0.5;
    }else if(_isRightKeyDown == true){
        _xVector += 0.5;
    }else{
        //どちらも押されていない場合には0を近づける
        _xVector += ( 0 - xVector ) * 0.3;
        if(Math.abs( 0 - _xVector ) < 1){
            _xVector = 0
        }
    }
    //衝突判定がブロックを突き抜けることを避けるため
    //絶対値を区切りの長さ-1に補正する
    if(20-1 < Math.abs(_xVector)){
        _xVector = (20-1)*((_xVector < 0) ? -1:1)
    }
}
//y方向の移動量を計算
function _calcYVector():void{
    //y方向の力を重力分加算する
    _yVector += 2.0;
    //衝突判定がブロックを突き抜けることを避けるため
    //絶対値を区切りの長さ-1に補正する
    if(20-1 < Math.abs(_yVector)){
        _yVector = (20-1) * ((_yVector < 0) ? -1 : 1);
    }
}

移動量の計算

補正を行う際には、区切りの長さを「-1」補正していますがこれはギリギリの位置にキャラクターが立った場合に、判定対象のマトリクス座標がブロックを通りこしてしまうことがあるためです。
以上が移動量計算の処理と補正になります。これで毎フレームの移動量が導き出せたので、あとは衝突判定と移動の実装を残すのみです。この実装が終われば、アクション部分の実装はほぼ完了です。

衝突判定と移動

衝突判定と移動の処理は、移動後のキャラクターの上下左右の端の座標を求め、その座標が属するマトリクス座標を調べます。その値が1だった場合は衝突しない位置で停止させ、0の場合には移動させるというものです。
サンプルでは先にy方向の処理を行った後、x方向の処理をするという流れになっています。これは、ジャンプで壁を超えやすくするための配慮です。このあたりは実装に依存してしまうので、必ずしもy方向の処理が先でなければならないわけではありません。実装方法やチューニング次第では、x方向を先に処理したり、同時に処理するほうがよい場合もあります。それでは実装のほうを見ていきましょう。

使用するローカル変数を定義する

衝突判定を行うには、移動後のキャラクターの上下左右の端となる座標が必要になります。

ローカル変数

サンプルでは、これらの値は一時的に保持するために、以下のようなローカル変数を定義しています。

//移動後のキャラクターの上端のy座標
var _top:Number;
//移動後のキャラクターの中心のy座標
var _center:Number;
//移動後のキャラクターの下端のy座標
var _bottom:Number;
//移動後のキャラクターの左端のx座標
var _left:Number;
//移動後のキャラクターの右端のx座標
var _right:Number;

それでは、これらのローカル変数を用いて、どのように移動処理を行うかを解説していきます。

y方向の移動処理

y方向の移動処理は、_yVectorが上下どちらを向いているのかで、処理を変えます。_yVectorがマイナスの場合は上向きの移動なので足元の衝突判定を行います。なお、処理の中で、getFlagという関数が何回か出てきますが、これはコンテンツ上の座標を引数に渡すとマトリクス上の座標に変換し、さらにその値を取得するための関数です。 また、分岐後の処理の中で、OR条件文がそれぞれに存在していますが、これはキャラクターの左右両端で衝突判定を行うために必要です。単純に頭上や足元と言ってもキャラクターは横幅があるので、キャラクターのx座標の中心のみで衝突判定を行ってしまうとブロックを突き抜けてしまうことがあります。例えば、下図のような場合に左脚がブロックに乗っているにもかかわらず、中心座標で調べるとブロックがないとみなされてしまいます。また、左右の座標を実際よりも4px補足しているのは、スクリプト内のコメントでも述べられているように、ギリギリで衝突判定をしてしまうと操作がシビアになってしまうため、多少間隔に余裕を持たせる必要があるためです。

地面

//y方向に移動後のキャラクターの判定領域を計算
_top = Math.floor( y ) + _yVector;
_bottom = Math.floor( y + height ) + _yVector;
//ブロックのスキマを通れるようにするため左右の判定を小さめにしておく
_left = Math.floor( x ) + 4;
_right = Math.floor( x + width ) - 4;
//移動先に障害物がないか調べる
if( 0 < _yVector) {
    //地面に立っているときや落下時
    //足元に地面があるかをチェックする
    if( 0 < getFlag _left,_bottom ) || 0 < getFlag _right,_bottom ) ) {
        //地面があった場合、
        //地面の位置で停止させる
        y = Math.floor( _top / 20)* 20;
        //y方向の力は地面で0になるので、_yVector を0にする
        _yVector = 0;
        //地面に立っているので、_isJumpingをfalseにする
        _isJumping = false;
    }else{
        //地面がなかった場合
        //移動させる
        y += _yVector;
    }//if
}else if(_yVector < 0){
    //ジャンプ上昇時
    //頭上に天井があるかをチェック
    if(0<getFlag(_left,_top) || 0<getFlag(_right,_top)){
        //天井があった場合
        //天井の位置で停止させる
        y=Math.ceil(_top/20)*20;
        _yVector = 0;
    }else{
        //障害物がなかった場合
        //移動させる
        y += _yVector;
    }
}//if( 0 < _yVector) 

x方向の処理

x方向の処理も、y方向と同様に、_xVectorの向きで処理を変えます。また、横の衝突判定の場合、上下に加えて中心の座標に対しても衝突判定を行っています。これも、y方向の処理と理由は同じで、 ブロックを突き抜けてしまうことを防ぐためです。また、上下の座標を縮めているのは、y方向の処理と同じく衝突判定に余裕を持たせるためです。

//x方向に働く力がない場合には、何もしない
if(_xVector == 0){
    return;
}
//x方向に移動後のキャラクターの判定領域を計算
//ブロックの間のスキマを通れるようにするため上下の判定は小さめにしておく
_top = Math.floor( y )+8;
_center = (Math.floor( y + height / 2 ) );
_bottom = Math.floor( y + height ) - 8;
_left = Math.floor( x )+_xVector;
_right = Math.floor( x + width ) + _xVector;
//移動先に障害物がないか調べる
if( 0 < _xVector){
    //右に移動している場合
    //自分の右に壁があるかをチェックする
    if(0 < getFlag(_right,_top)) ||
    if(0 < getFlag(_right,_center)) ||
    if(0 < getFlag(_right,_bottom)) {
        //壁があった場合
        //壁の位置で停止させる
        x = Math.floor(_left / 20) * 20;
        _xVector = 0.5;
    }else{
        //壁がなかった場合
        //移動させる
        x += _xVector;
    }
}else if(_xVector < 0){
    //左に移動している場合
    //自分の左に壁があるかをチェック
    if(0 < getFlag(_left,_top)) ||
    if(0 < getFlag(_left,_center)) ||
    if(0 < getFlag(_left,_bottom)) {
        //壁があった場合
        //壁の位置で停止させる
        x = Math.ceil(_left / 20) * 20;
        _xVector = 0.5;
    }else{
        //壁がなかった場合
        //移動させる
        x += _xVector;
}

以上が衝突判定の処理になります。これで、要点となる「衝突判定」と「キャラクターの操作」部分の解説は終わりです。これ以外にも、キャラクターのアニメーション(ジャンプ中のポーズなど)を制御する処理などがありますが、それらの実装はサンプルを見て確かめてみてください 。

プロフィール

プロフィール
名前 ぬえさぶろう
仕事 PCの修理受付・取扱説明書作成
好物 大豆食品
スキル jQuery

翔泳社

toTop