んななのゲーム開発備忘録

ゲーム制作の進捗と開発メモなど掲載しています。

Unityで斜面に沿ったベクトルはVector3.ProjectOnPlaneで作ろう+その解説

f:id:nyanya-nnana:20171219223326p:plain:w500

キャラクターに力を加えたい時、斜面に沿って加えたいと思ったことはありませんか? 本記事ではUnityで斜面など平面に沿ったベクトルを簡単に作ることができるVector3.ProjectOnPlaneと、平面に沿ったベクトルの作り方について解説します。 手っ取り早く平面に沿ったベクトルを実装したい方はコチラの手順を見ればおっけーです。

  • 沿わせたい平面の法線ベクトル(normalVector)を用意(法線ベクトルは平面に対して垂直なベクトル)
  • 沿わせたいベクトル(inputVector)を用意
  • 目的のベクトル(onPlane)をVector3.ProjectOnPlaneを用いて次のように計算
onPlane = Vector3.ProjectOnPlane(inputVector, normalVector);

目次

平面に沿ったベクトルの実装

平面に沿ったベクトルの実装方法は色々あるかと思いますが、次の3つを上げておこうと思います。

どのようにして作られるか知りたい方はコチラを参照してください。

このベクトルを使用したデモはコチラをご覧ください。

Vector3.ProjectOnPlane(Vector3 vector, Vector3 planeNormal)を使う方法

実はしたいことそのもののメソッド(Vector3.ProjectOnPlane(Vector3 vector, Vector3 planeNormal))が用意されています。

1つ目の引数のvectorは平面に投影したいベクトルで、2つ目の引数のplaneNormalは平面の法線ベクトルです。メソッドを実行するとVector3型の平面に沿ったベクトルが返ってきます。

次のように使用します。

Vector3 normalVector = Vector3.zero;

private void OnCollisionStay(Collision collision)
{
    // 衝突した面の、接触した点における法線を取得
    normalVector = collision.contacts[0].normal;
}

private void FixedUpdate()
{
    // 平面に投影したいベクトルを作成
    Vector3 inputVector = Vector3.zero;
    inputVector.x = Input.GetAxis("Horizontal");
    inputVector.z = Input.GetAxis("Vertical");

    // 平面に沿ったベクトルを計算
    Vector3 onPlane = Vector3.ProjectOnPlane(inputVector, normalVector);
}

Vector3.Project(Vector3 vector, Vector3 onNormal)を使う方法

こちらはVector3.Project(Vector3 vector, Vector3 onNormal)というメソッドを用いる方法です。

平面に投影したいベクトルの法線方向の成分のベクトルを次のように計算して

Vector3 onNormal = Vector3.Project(inputVector, normalVector);

得られた法線方向成分のベクトルを元のベクトルから引くと所望のベクトルが得られます。 Vector3.ProjectOnPlaneと共通する部分は省略しています。

Vector3 onPlane = inputVector - onNormal;

Vector3.Dot(Vector3 lhs, Vector3 rhs)を使う方法

最後はベクトルの内積を求めるメソッドVector3.Dot(Vector3 lhs, Vector3 rhs)を用いる方法です。 最初に平面に沿わせたいベクトルの法線方向の成分の大きさを内積を使って計算します。法線方向のベクトルの大きさが1でない場合はnormalizedされたベクトルを使用してください。

// normalVectorの大きさが1でない場合はnormalVectorの代わりにnormalVector.normalizedを使用
float inputMagnitudeOnNormal = Vector3.Dot(inputVector, normalVector);

次に法線方向の成分のベクトルを計算します。

Vector3 onNormal = inputVector - inputManigtudeOnNormal*normalVector;

このベクトルを平面に沿わせたいベクトルから引けば所望のベクトルが得られます。

Vector3 onPlane = inputVector - onNormal;

デモ

斜面を考慮しないで一定方向に力を加えた場合、次の動画のようになります。

斜面でひっかかって登れません。


Unityで斜面に沿ったベクトルはVector3.ProjectOnPlaneで作る(その1)

こちらはVector3.ProjectOnPlaneを使って斜面に沿ったベクトルを計算したものです。 斜面でひっかからずにスムーズに登ることができています。


Unityで斜面に沿ったベクトルはVector3.ProjectOnPlaneで作る(その2)

平面に沿ったベクトルの作り方

平面に沿ったベクトルの作り方は次の通りです。

"ベクトルは、他のベクトルの足し算で表現できる"という性質を用いて平面に沿ったベクトルを作成します。 この性質を用いると平面に沿わせたいベクトルinputVectorは、平面に沿った方向のベクトルonPlaneと、その法線方向のベクトルonNormalという2つのベクトルの足し算として次のように表すことができます。

inputVector = onPlane + onNormal

図に表すと次の通りです。

f:id:nyanya-nnana:20171219124719p:plain:w400

この式を変形することで次のように平面に沿ったベクトルが計算できます。

onPlane = inputVector - onNormal

つまり平面の法線方向のベクトルさえ得られれば、これに沿うベクトルが得られるということです。

問題はどのようにしてそれを得るか?です。 これにはベクトルの内積を用います。

ベクトルの内積を使って求めたい方向の成分を計算

ベクトルの内積はベクトル\boldsymbol{r}\boldsymbol{x}と、これらのベクトルがなす角を\thetaとして、次のように表せます。

\boldsymbol{r}\cdot\boldsymbol{x} = |\boldsymbol{r}||\boldsymbol{x}|\cos{\theta}

内積の演算は\cdotで表され、|\boldsymbol{r}|はベクトル\boldsymbol{r}の大きさを表しています。 \cos{\theta}余弦関数です。 UnityのC#的に表すと次のようになります。

Vector3.Dot(vectorR, vectorX) = vectorR.magnitude * vectorX.magnitude * Mathf.Cos(angleTheta)

もう少し詳しく知りたい方はコチラの記事などを参考にしてみるとよいかもしれません。

f:id:nyanya-nnana:20171219131557p:plain:w300

余弦関数は上の図のように円上に点をとって図形を考えると、円の半径をrx軸方向の成分をxとすると次のように表せます。

\cos{\theta} = x/r

これを変形すると

x = r\cos{\theta}

つまり、xを求めたい方向の成分と考えると、元のベクトルの長さと余弦関数を用いると求めたい方向の成分が求められることがわかります。

これを内積と比較してみると似たような形をしていることがわかります。

ベクトル\boldsymbol{r}を元のベクトル、ベクトル\boldsymbol{x}を求めたい方向のベクトルと考えるとさきほどの式にベクトルxの大きさをかけたものがベクトルの内積になっています。

そこで、ベクトル\boldsymbol{x}の大きさを1としてみます。

そうすると、ベクトルの内積を用いて求めたい方向の成分inputMagnitudeOnNormalを計算できるようになります。

inputMagnitudeOnNormal=|\boldsymbol{r}|\cos{\theta}

UnityのC#的に表すと次のようになります。

magnitudeOnNormal = vectorR.magnitude * Mathf.Cos(angleTheta)

法線方向のベクトルを作成

先の計算で得られた法線方向成分の大きさを、大きさ1の法線ベクトルにかけると法線方向成分のベクトルonNormalが得られます。

onNormal = inputMagnitudeOnNormal * normalVector.normalized

元のベクトルから法線方向のベクトルを引いて平面に沿ったベクトルを計算

元のベクトルinputVectorは法線方向のベクトルonNormalと平面に沿ったベクトルonPlaneを足し算で表されていたことを思い出すと、求めるベクトルは次のように計算することができます。

onPlane = inputVector - onNormal

記事の内容は以上となります(。・x・)ゞ

Unityの衝突判定を数値化して不安定な接触を安定化する(デリゲート編)

f:id:nyanya-nnana:20171201002933p:plain:w500
Unityの衝突判定を数値化して不安定な接触を安定化する
キャラクターが接地しているか判定するときに思った通りにならなくて困ったことはありませんか? 今回の記事ではそんな時に役立つ、接触が不安定な場合でも安定して接触を判定する処理について説明します。

本記事で紹介する接触判定クラスはGithub (https://github.com/nyanya-nnana/CollisionDetector)にソースをあげてあります。

こちらの記事は前記事のUniRxの代わりにDeligateを利用し実装したものとなります。本ソースコード以外はとくにusingしないで使用できるようになりました。

本記事ではDeligateを使用したバージョンの使用方法とその実装について紹介します。 衝突判定の安定化の原理等の共通内容は前記事を参照してください。

手っ取り早く使いたいという方は次の手順をどうぞ。

  • CollisionDetectorコンポーネントを接触判定したいGameObjectにアタッチ
  • 接触対象のタグをチェックする場合はCheckTargetTagをtrueにして対象のタグをTargetTagで設定
  • 接触判定を使用したいスクリプトでCollisionDetectorをGetComponent
  • OnCollisionEnterで処理したいメソッド(戻り値void、引数Collision)をonCollisionEnterSmoothedに追加
  • OnCollisionStayで処理したいメソッド(戻り値void、引数Collision)をonCollisionStaySmoothedに追加
  • OnCollisionExitで処理したいメソッド(戻り値void、引数Collision)をonCollisionExitSmoothedに追加
  • onCollisionStaySmoothedでOnCollisionStayでしたい処理を追加
  • onCollisionExitSmoothedでOnCollisionExitでしたい処理を追加

CollisionDetectorクラスの使用方法

CollisionDetectorの設定

衝突判定をしたいGameObjectにCollisionDetectorをAddComponentしてください。 CollisionDetectorの設定項目は次の通りです。

Check Ohter Tag

衝突したGameObjectのタグをチェックするかしないか設定できます。

Target Tag

Check Other Tagがtrueの場合はここにTargetのタグを設定します。

Cut Off Frequency

衝突判定の安定度。低いほど判定の変化がゆっくりになります。

Detection Threshold Upper/Lower

衝突判定の閾値。2つの差が大きいほど変化しにくくなります。

衝突判定の利用

使用したいスクリプト内で

using NnanaSoft.CollisionDetector;

を宣言してください。 あとはStartかAwake内でGetComponentして 処理したいメソッドを次のようにデリゲートに追加すればおっけーです。 追加するメソッドの戻り値はvoid型、引数はCollision型にしてください。

private void Start()
}
    var SmoothedCollision = GetComponent<CollisionDetector>();

    // OnCollisionEnter
    SmoothedCollision.onCollisionEnterSmoothed += OnCollisionEnterMethod;
    // OnCollisionStay
    SmoothedCollision.onCollisionStaySmoothed += OnCollisionStayMethod;
    // OnCollisionExit
    SmoothedCollision.OnCollisionEnterSmoothed += OnCollisionExitMethod;
}
// OnCollisionEnter
private void OnCollisionEnterMethod(Collision collision)
{
    // ここに処理を記述
    Debug.Log("OnCollisionEnterSmoothed");
}
// OnCollisionStay
private void OnCollisionStayMethod(Collision collision)
{
    // ここに処理を記述
    Debug.Log("OnCollisionStaySmoothed");
}
// OnCollisionEnter
private void OnCollisionExitMethod(Collision collision)
{
    // ここに処理を記述
    Debug.Log("OnCollisionExitSmoothed");
}

使用するとこんな感じになります。


CollisionDetector - デモ

CollisionDetectorクラスの実装

CollisionDetectorクラスのソースコードGithubのコチラをご覧ください。

安定化した衝突判定のイベント通知にはデリゲートを使って実装します。

// 衝突時の処理用デリゲート
public delegate void OnCollisionEnterSmoothedDelegate(Collision collision);
public delegate void OnCollisionStaySmoothedDelegate(Collision collision);
public delegate void OnCollisionExitSmoothedDelegate(Collision collision);
// 衝突時の処理を追加する
public OnCollisionEnterSmoothedDelegate onCollisionEnterSmoothed;
public OnCollisionStaySmoothedDelegate onCollisionStaySmoothed;
public OnCollisionExitSmoothedDelegate onCollisionExitSmoothed;

衝突時の処理用のデリゲートを用意して、そのデリゲートを宣言します。 対応するデリゲートに使用したいメソッドを加えることで該当のイベント時にメソッドが実行されます。

生の衝突判定とその時の衝突情報はOnCollisionEnter、Stay、Exitの処理中に更新して、これを使って衝突判定を数値化します。

// OnCollisionEnter
private void OnCollisionEnter(Collision collision)
{
    // タグが一致しない場合は何もしない
    if (checkOtherTag == false || other.gameObject.CompareTag(targetTag) == true)
    {
        _onCollisionRaw = true;
        _otherOnEnter = collision;
        _otherOnStay = collision;
    }
}
// OnCollisionStay
private void OnCollisionStay(Collision collision)
{
    // タグが一致しない場合は何もしない
    if (checkOtherTag == false || other.gameObject.CompareTag(targetTag) == true)
    {
        _otherOnStay = collision;
    }
}
// OnCollisionExit
private void OnCollisionExit(Collision collision)
{
    // タグが一致しない場合は何もしない
    if (checkOtherTag == false || other.gameObject.CompareTag(targetTag) == true)
    {
        _onCollisionRaw = false;
        _otherOnExit = collision;
    }
}

衝突状態を1、していない状態を0としてFixedUpdate内で衝突判定を数値化します。_filterConstantはローパスフィルタの定数aにあたります。

// 衝突しているかどうかを数値化。_filterConstantが大きいほど1サイクル前の状態を優先
_onCollisionFloat = (1.0f - _filterConstant) * onCollisionCurrent + _filterConstant * _onCollisionFloat;

次のように衝突判定の変化にヒステリシスを持たせて、

if (onCollisionPrevious)
{
    // 1つ前のサイクルで衝突している場合、下限閾値を下回るまで状態を変化させない
    if (_onCollisionFloat <= detectionThresholdLower)
        _onCollision = false;
    else
        _onCollision = true;
}
else
{
    // 1つ前のサイクルで衝突していない場合、上限閾値を上回るまで状態を変化させない
    if (_onCollisionFloat >= detectionThresholdUpper)
        _onCollision = true;
    else
        _onCollision = false;
}

OnCollisionEnter、Stay、Exitになったときにデリゲートメソッドを実行します。

// OnCollisionEnterか、Stayか、Exitか判定
if (_onCollision == onCollisionPrevious)
{
    _onCollisionEnter = false;
    _onCollisionExit = false;
}
else
{
    if (_onCollision)
    {
        // OnNextでイベント通知
        _onCollisionEnter = true;
        OnCollisionEnterSmoothedDelegate(onCollisionEnterSmoothed);
    }
    else
    {
        // OnNextでイベント通知
        _onCollisionExit = true;
        OnCollisionExitSmoothedDelegate(onCollisionExitSmoothed);
    }
}
_onCollisionStay = _onCollision;
// OnNextでイベント通知
if (_onCollisionStay)
{
    OnCollisionStaySmoothedDelegate(onCollisionStaySmoothedDelegate);
}

以上がCollisionDetectorの主な処理です。

Unity1週間ゲームジャム「space」に参加しました

だいぶ遅くなっちゃいましたが11月13日~19日までUnity1週間ゲームジャムに参加してきたのでその振り返り記事を書こうと思います。

目次

Unity1週間ゲームジャムって?

Unity1週間ゲームジャムはその名前の通り1週間かけて1つのゲームを作ろうというイベントで、 今年の4月から定期的に開催されてるみたいです。

次回はまだ未定みたいですけど、たぶん1月か2月くらいには開催されるんじゃないかなと思います。 1週間でゲームが完成しなくても投稿しても大丈夫という、初心者や忙しい人に優しいとても懐の深いイベントなので興味がわいた方は是非参加してみてください!

開催中は#unity1weekのタグでTwitter上でに参加者の進捗が見れたりして楽しいですよ(#'w'#)

1日目 ― ゲームの構想

まずは「space」というテーマに沿ってどういうゲームを作ろうか考えました。 spaceっていうと宇宙だとか空間だとか意味がありますけど真っ先に浮かんだのは空間の意味でのspaceでした。

空間というワードでまず思ったのはこう「狭いところに丸く収まりたい」でした。 そういうの好きなんですよね。

狭いところに"自分が"収まるとなると、天井から大きな岩が降りてきてそれの隙間にうまくおさまる っていうのが思いつくんですけど、それってたぶん他の人もやるだろうし、その方向で進めてみてもよいアイデアが浮かばなかったのですっぱり諦めました。

それでどうしたかというと、狭いところに収まるのは"自分"ではないという方向性でした。 狭いところに"何か"を収めるという方向で考えました。

「狭いところに"何か"を収める」っていうワードなので、主人公となるプレイヤーはきっと正義の味方ではないよね、 っていうのと、あと前のUnity1週間ゲームジャムの「積む」の時から地獄をテーマにしたゲームを作ってみたいと思っていたのがあって主人公は悪魔にしました。

悪魔が狭いところに収めるっていうと「騙す」んだろうなって思いました。

では"何を"騙すか?っていうとなんだろう... そういえばspaceって宇宙って意味もあるよねと思って、月につながって、結果的にウサギを騙すことにしました。

ウサギといえばニンジン、それで騙して収める狭いところといったら落とし穴だよね。

こんな感じでゲームの構想が固まっていきました。わりとすんなりでした。

Unity1週間ゲームジャムには毎回参加しているんですけど、いつもゲームの構想に時間がかかっちゃってタイムアウトになっちゃうんですよね。

ってことで構想が固まって、1日目は残りの時間で素材づくりをはじめました。

2日目~3日目

ひたすら素材作りをしていました。3Dモデルの作成と、そのモーション作成です。

かわいい雰囲気を全面的に押し出したかったのでその方向で作りました。

4日目~6日目

素材も作ったのでUnity上で挙動づくりを頑張ってました。改めて見てみると実装に時間がかかってますね(。×﹏×。) 再利用可能なスクリプトを準備したりしてもう少し早く実装できるようにしないとですね。

7日目

ちょっと遅れてしまったものの何とか1週間でゲームができて投稿できました!よかったー

使用したアセット

UniRx - Reactive Extensions for Unity

作るゲームには毎回入れています。 イベント通知したり処理がすごく楽ちんです。

ReactivePropertyっていう変数と同じように使えるけど値が変化したときにイベントも飛ばせるものがあってそれがすごく便利です!

Farland Skies - Cloudy Crown

手軽にかわいい雰囲気の空が作れます。カラーバリエーションがあるのでそこもいいです。

Character Particle

ウサギがニンジンニンジン!って喋っているのはこのアセットで作ってます。 簡単にゲームのキャラクターを演出できて気に入ってます。

f:id:nyanya-nnana:20171210131058p:plain:w500
Character Particleを使ったウサギのセリフづけ

TextMesh Pro

タイトルとかの文字に使いました。標準のテキストと違って輪郭がぼけないのがいいですね。

感想

いつも思うことなんですけど、Unity1週間ゲームジャムは普通のゲームジャムの短期集中とは違ってまとまった時間がとれない人でも参加しやすくって本当によいイベントだなーって思います。

あと、主催者のnaichiさんが遅れてしまったり、未完成でも投稿してかまわないよといってくださっているのがすごく参加しやすいなと思います。

Unity1週間ゲームジャムに参加するときは毎回技術的な目標を立てて参加するんですけど、うまく行かないことが多くって、それでも継続的に参加できているのはその言葉のおかげだと思います。

今回の目標は1週間以内に3Dモデルを作ってそれを動かしてゲームにするっていうことだったので、それができて一安心です。 ただ、操作が難しいっていうご指摘をいっぱい受けたので、次に参加する時はもっとシステム面に力を入れられるようにしたいな(っ ` -´ c)

ということで今回のUnity1週間ゲームジャム「space」の振り返りを締めようと思います。

次回も絶対参加するぞー!(۶•̀ᴗ•́)۶

Unityの衝突判定を数値化して不安定な接触を安定化する(UniRx編)

f:id:nyanya-nnana:20171201002933p:plain:w500
Unityの衝突判定を数値化して不安定な接触を安定化する
キャラクターが接地しているか判定するときに思った通りにならなくて困ったことはありませんか? 今回の記事ではそんな時に役立つ、接触が不安定な場合でも安定して接触を判定する処理について説明します。

本記事で紹介する接触判定クラスはGithub (https://github.com/nyanya-nnana/CollisionDetector)にソースをあげてあります。 よければ使用してみてください。使用するにはUniRxが必要です。UniRxを使用しないバージョンはコチラで紹介しています。 もし要望等あればお気軽に。

※ローパスフィルタの数式のc_{\rm{raw}}c_{\rm{prev}}が逆になっていたので修正しました(2017/12/03)

手っ取り早く使いたいという方は次の手順をどうぞ。

  • UniRxをAsset Store (https://www.assetstore.unity3d.com/jp/#!/content/17276)からインポート
  • CollisionDetectorコンポーネントを接触判定したいGameObjectにアタッチ
  • 接触対象のタグをチェックする場合はCheckTargetTagをtrueにして対象のタグをTargetTagで設定
  • 接触判定を使用したいスクリプトでCollisionDetectorをGetComponent
  • OnCollisionEnterSmoothedAsObservableでOnCollisionEnterでしたい処理を記述
  • OnCollisionStaySmoothedAsObservableでOnCollisionStayでしたい処理を記述
  • OnCollisionExitSmoothedAsObservableでOnCollisionExitでしたい処理を記述

目次はコチラ

接触判定が不安定になる要因

あるゲームオブジェクトに別のゲームオブジェクトが接触が始まった時にはMonobehaviourのOnCollisionEnterを使ってその時の処理を書くと思います。 接触中の処理も同様にOnCollisionStayを使うと思います。

でも、接触してそうなのにOnCollisionEnterとOnCollisionExitを繰り返していて安定しない、そんなときがあると思います。

原因はゲームオブジェクトが衝突が繰り返されるからです。相手のゲームオブジェクトに近づいて、跳ね返って、力がかかってるからまた近づいて、これの繰り返しが原因です。

f:id:nyanya-nnana:20171202201315p:plain:w400
揺れてる?
これは物理現象なので仕方ありません。 なので、接地の判定をするときは接地判定用のis TriggerなColliderをアタッチしてそれを使って通常は判定します。 こんな感じに。
f:id:nyanya-nnana:20171202201323p:plain:w400
トリガーがあれば揺れてもだいじょうぶ。でも立ってないときは...?
このようにTriggerのColliderを用意すると簡単に安定して接地の判定ができます。 でも、キャラクターが攻撃されて寝転んだ時など、足から接地しない場合はうまく判定できなくて、工夫しないといけません。

今回紹介したいのは、そういうことをしなくても衝突判定にちょっとフィルタをはさんであげればOnCollisionのイベントだけで案外安定してとれるんだよってお話です。

接触判定を安定させる原理

この項は数式が苦手な人は読み飛ばしちゃっても大丈夫です。 CollisionDetectorの実装はコチラ、使用方法はコチラ

ローパスフィルタ

接触判定の安定には1次のローパスフィルタというものを使用します。 式で表すとこんな感じ。

c = (1-a)c_{\rm{raw}} + ac_{\rm{prev}}

フィルタ処理した現在の接触判定が c、1サイクル前の接触判定が c_{\rm{prev}}、フィルタ処理していない生の接触判定が c_{\rm{raw}}です。

c_{\rm{raw}}と、c_{\rm{prev}}のどちらを優先するか、定数aを使って決定します。 aが大きいほど前の値を優先するのでc_{\rm{raw}}が激しく変化していてもあまり変化しません。

定数aはカットオフ周波数f_\rm{c}とサンプリング周波数T_\rm{s}というものを使って次のように表現します。

a = e^{-f_\rm{c}T_\rm{s}}

カットオフ周波数f_c以上の速い動きに影響されなくなるって認識で概ね大丈夫です。 サンプリング周波数にはTime.fixedDeltaTimeが入ります。 具体的な値としてf_\rm{c} = 5Hzを入れて接触判定をとってみると次のようになります。


CollisionDetector - ローパスフィルタの効果

カットオフ周波数が低いと生の接触判定c_{\rm{raw}}(緑色)が激しく変化しても水色の接触判定がゆるやかに追従しています。 これを使って接触判定の変化をマイルドにします。

ヒステリシス

ローパスフィルタを使うと接触判定の急激な変化をおさえられることがわかりました。 でも、それだけだと安定した接触判定は実現できません。 それはなぜかというと、次のようなことが起こるからです(f_\rm{c} = 30Hz)。


CollisionDetector - ヒステリシスなし

接触していないときを0、しているときを1として、0.6以上で接触していると判断した場合の接触判定をしていますが、 0.6を上回ったり下回ったりして判定が安定しません。 そこでヒステリシスというものを用います。

f:id:nyanya-nnana:20171202201333p:plain:w400
ヒステリシスの説明図
「一度接触していると判定されたら、逆の判定をする条件を厳しくする」ということをするために用います。 判定変化の条件が、trueになるときと、falseになるときで変えてしまえば判定が簡単には変化しなくなります。 これを用いて接触判定の上限を0.6、下限を0.1に設定すると次のように接触判定が安定します。


CollisionDetector - ヒステリシスあり

CollisionDetectorクラスの実装

CollisionDetectorクラスの実装にはUniRxというライブラリを使用しています。

これを使うとイベントが起きたときの処理が簡潔に書けます。

CollisionDetectorクラスのソースコードGithubのコチラをご覧ください。 長くなるので使用方法を知りたい方はコチラをどうぞ。

UniRxではイベントを通知するためのストリームのソースを作る方法がいくつかあります(コチラが参考になります)が、今回はSubjectとIObservableを使ってイベント通知を実装します。

// Subject:イベント発行
private Subject<Collision> _onCollisionEnterSubject = new Subject<Collision>();
private Subject<Collision> _onCollisionStaySbuject = new Subject<Collision>();
private Subject<Collision> _onCollisionExitSubject = new Subject<Collision>();
// IObservable:イベント購読
public IObservable<Collision> OnCollisionEnterSmoothedAsObservable
{
    get { return _onCollisionEnterSubject; }
}
public IObservable<Collision> OnCollisionStaySmoothedAsObservable{
    get { return _onCollisionStaySbuject; }
}
public IObservable<Collision> OnCollisionExitSmoothedAsObservable
{
    get { return _onCollisionExitSubject; }
}

SubjectはOnNextというメソッドを持っていてこれを実行すると対応するIObservableにイベントのメッセージが通知されます。

生の衝突判定とその時の衝突情報はOnCollisionEnter、Stay、Exitの処理中に更新して、これを使って衝突判定を数値化します。

// OnCollisionEnter
this.OnCollisionEnterAsObservable()
.Subscribe(other =>
{
    // タグが一致しない場合は何もしない
    if (checkOtherTag == false || other.gameObject.CompareTag(targetTag) == true)
    {
        _onCollisionRaw = true;
        _otherOnEnter = other;
        _otherOnStay = other;
    }
});
// OnCollisionStay
this.OnCollisionStayAsObservable()
.Subscribe(other =>
{
    // タグが一致しない場合は何もしない
    if (checkOtherTag == false || other.gameObject.CompareTag(targetTag) == true)
    {
        _otherOnStay = other;
    }
});
// OnCollisionExit
this.OnCollisionExitAsObservable()
.Subscribe(other =>
{
    // タグが一致しない場合は何もしない
    if (checkOtherTag == false || other.gameObject.CompareTag(targetTag) == true)
    {
        _onCollisionRaw = false;
        _otherOnExit = other;
    }
});

OnCollisionEnterAsObservableはOnCollisionEnterをUniRx的に使えるようにしたもので、SubScribeのあとに続くブロックの中で行いたい処理を書きます。

Subscribe内ではラムダ式というものを使っていて、最初にotherと名前をつけた変数に衝突の情報が入っているようです。

衝突状態を1、していない状態を0としてFixedUpdate内で衝突判定を数値化します。_filterConstantはローパスフィルタの定数aにあたります。

// 衝突しているかどうかを数値化。_filterConstantが大きいほど1サイクル前の状態を優先
_onCollisionFloat = (1.0f - _filterConstant) * onCollisionCurrent + _filterConstant * _onCollisionFloat;

次のように衝突判定の変化にヒステリシスを持たせて、

if (onCollisionPrevious)
{
    // 1つ前のサイクルで衝突している場合、下限閾値を下回るまで状態を変化させない
    if (_onCollisionFloat <= detectionThresholdLower)
        _onCollision = false;
    else
        _onCollision = true;
}
else
{
    // 1つ前のサイクルで衝突していない場合、上限閾値を上回るまで状態を変化させない
    if (_onCollisionFloat >= detectionThresholdUpper)
        _onCollision = true;
    else
        _onCollision = false;
}

OnCollisionEnter、Stay、ExitになったときにOnNextを使って衝突情報を持ったイベントを通知します。

// OnCollisionEnterか、Stayか、Exitか判定
if (_onCollision == onCollisionPrevious)
{
    _onCollisionEnter = false;
    _onCollisionExit = false;
}
else
{
    if (_onCollision)
    {
        // OnNextでイベント通知
        _onCollisionEnter = true;
        _onCollisionEnterSubject.OnNext(_otherOnEnter);
    }
    else
    {
        // OnNextでイベント通知
        _onCollisionExit = true;
        _onCollisionExitSubject.OnNext(_otherOnExit);
    }
}
_onCollisionStay = _onCollision;
// OnNextでイベント通知
if (_onCollisionStay)
    _onCollisionStaySbuject.OnNext(_otherOnStay);

以上がCollisionDetectorの主な処理です。

CollisionDetectorクラスの使用方法

UniRxの導入

CollisionDetectorはUniRxというライブラリを使用しています。 まずはそちらをUnity Asset StoreからインポートもしくはGithubから導入してください。

CollisionDetectorの設定

導入が終わったら、衝突判定をしたいGameObjectにCollisionDetectorをAddComponentしてください。 CollisionDetectorの設定項目は次の通りです。

Check Ohter Tag

衝突したGameObjectのタグをチェックするかしないか設定できます。

Target Tag

Check Other Tagがtrueの場合はここにTargetのタグを設定します。

Cut Off Frequency

衝突判定の安定度。低いほど判定の変化がゆっくりになります。

Detection Threshold Upper/Lower

衝突判定の閾値。2つの差が大きいほど変化しにくくなります。

衝突判定の利用

使用したいスクリプト内で

using NnanaSoft.CollisionDetector;

を宣言してください。 あとはStartかAwake内でGetComponentして

    var SmoothedCollision = GetComponent<CollisionDetector>();

その中で行いたい処理を記述すればおっけーです。

// OnCollisionEnter
SmoothedCollision.OnCollisionEnterSmoothedAsObservable
    .Subscribe(other =>
    {
        // ここに処理を記述
        Debug.Log("OnCollisionEnterSmoothed");
    });
// OnCollisionStay
SmoothedCollision.OnCollisionStaySmoothedAsObservable
    .Subscribe(other =>
    {
        // ここに処理を記述
        Debug.Log("OnCollisionStaySmoothed");
    });
// OnCollisionExit
SmoothedCollision.OnCollisionEnterSmoothedAsObservable
    .Subscribe(other =>
    {
        // ここに処理を記述
        Debug.Log("OnCollisionExitSmoothed");
    });

使用するとこんな感じになります。


CollisionDetector - デモ

デジゲー博2017に参加してきました

定期的に記事を投稿したいと言いつつ2ヶ月(あともうちょっとで3ヶ月)も経ってしまいました(´-﹏-`;)
余裕が無いからあとまわし、が繰り返された感じです。これは良くない。余裕は作らないとですね。

前置きはおいといて、だいぶ遅くなりましたが11月12日(日)にデジゲー博で展示側として参加してきたので、
今日はその振り返りと、あと気づいたことを記事を書こうと思います(。・x・)ゞ

 

当日は「ひと夏のぼうけん」というゲームを展示しました↓

ちなみに展示したかったものは

・振って遊べるコントローラでのゲーム体験

・いろいろな虫が増えた体験版

だったんですけど、全然進捗が出ず結局上のツイートのバージョンをそのまま持っていくような形になってしまいました(。×﹏×。)

来てくださった方にも申し訳ないし、抽選で落選された方にも申し訳ないしで展示が始まった最初のあたりは精神的にひどい状態でした(´-﹏-`;)

時間の流れがそれはもうすごく遅く感じました。

 

でも、最初にゲームを遊びに来てくれた方がいて、その後はいつの間にか展示の終わりの時間になってました!

 改めてここに感謝いたします。本当にありがとうございました!

 

あとは展示していて気づいた点を今後の参考のためにまとめておきます!(とりあえず箇条書きですが...!)

  • 展示で使用するパソコン、モニタ等は立った状態で遊んでいただくのでそれに合わせた高さに設置したほうがいい。
  • 誰も遊んでいない時はデモ動画が流れるようにしたほうがよい:タイトル画面そのままよりは何か遊んでいただいてゲーム画面が動いているときのほうが断然反応がよかったです!
  • 「遊んでみませんか?」など声掛けは大事:遊んでみたいけど遊んでいいのかな?ってちょっと気後れする方もいらっしゃるみたいなので積極的に声を掛けていくべきだなって思いました!
  • ゲームの操作説明は展示者が説明する必要がないくらいわかりやすい図を用意しておくのがよい:操作説明については途中で気づいてかんたんな図を描いたんですけど体験までのハードルは可能な限り低くしたほうがよいなと思いました!
  • コントローラで遊べるところは強い:結局展示の対応で精一杯であまり周りを見て回れなかったのですが、コントローラと、プレイ画面を移した大きなモニタがあるところは人がずっといた印象でした。
  • ネガティブな展示はやめよう:展示の最初は申し訳無さでいっぱいでそれを展示の説明のホワイトボードに書いてたんですけど、誰の得にもならないのですぐにやめるべきです。実際やめたら人が遊びに来てくれやすくなった気がします...!(そんなの私のところだけかもですけどね(´-﹏-`;))
  • 遊んでもらっているときは結構見てくれる方が多い:んだけれど、プレイ時間が長いと見てくれる方もよそへ行っちゃいがちでした。適度な試遊時間を設定してゲームを体験していただくのが大事だなって思いました!
  • TwitterのIDとかアイコンとかあるとよい:この人かーってきっかけで見てもらいやすい気がしました。
  • 名刺はちゃんと用意しよう:展示している他の方との交流はもちろん名刺をいただく機会が多かったです。今回作れなかったのですがだいぶ失態です。次はちゃんと作っていきたいと思います!

ゲームを遊んでいただいて、たくさんの改善点や実装したら面白そうな要素が見つかりましたけど、それは内に秘めておくことにします...!乞うご期待...です!

 

とりあえず、ゲームを面白くしていくには技術力が全然足りていない!ってことはわかりました。

展示した「ひと夏のぼうけん」は来年夏リリース予定なのですが、間違いなく大規模化するので、プログラムの実装方法とかを検討しておかないと、開発の最後の方でぐだっちゃいそうな印象を持ちました。

なので、まずはそれより小規模なゲームを作って、そこで経験を積んでから「ひと夏のぼうけん」のプログラムの実装に移りたいと思ってます!

 

ちなみにそのゲームはコチラ↓

 プレイヤーが操作する悪魔と、動物と、天使が登場するふんわりブラック?なアクションゲームという名目で開発を進めていきます。こちらもよろしくお願いします!

 

ということで、長くなりましたがこれで振り返りを締めたいと思います。

ありがとうございましたー!

UnityでRigidBodyのAddForce()に速度制限をつけてすーっと動かしてすーっと止める

UnityではRigidBodyコンポーネントのAddForce()で力を加えて徐々に加速させられます。
どんどん加速していくのでゲームに使う時は速度制限が必要です。
そんな時に使えるTipsです。

手っ取り早く使いたい方は

RigidBody _rb;
Vector3 _moveVector;                  // 移動速度の入力
public float moveForceMultiplier;     // 移動速度の入力に対する追従度
_rb.AddForce(moveForceMultiplier * (moveVector - _rb.velocity));

を使えばおっけーです。moveVectorは目的に応じて中身が変わります。
これを使うとどう動くか見たい場合は記事の一番最後の動画を見てみてください(。・ω・。)


どういうもの?

RigidBodyのAddForce()を使ってキャラクターなどを移動する際は最大の移動速度を決めておいて、
それ以上の速度になった時にAddForce()をやめるっていう方法をよく見ます。

簡単でわかりやすい方法ですが、キャラクターを止める力が働かないので移動速度が速くなってしまうと
全然止まらないのが欠点だと思います。

今回紹介するのはAddForceの引数に移動させたい速度の入力と、現在の移動速度を含めることで
すーっと動きはじめて、すーっと動きが止まる、そんな移動方法です。


実装

ソースコードは次の通り。

RigidBody _rb;
public bool isUseCameraDirection;    // カメラの向きに合わせて移動させたい場合はtrue
public float moveSpeed;              // 移動速度
public float moveForceMultiplier;    // 移動速度の入力に対する追従度
public GameObject mainCamera;
float _horizontalInput;
float _verticalInput;

void Start()
{
    _rb = GetComponent<RigidBody>();

    if(mainCamera == null)
        mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
}

void Update()
{
    _horizontalInput = Input.GetAxis("Horizontal");
    _verticalInput = Input.GetAxis("Vertical");
}

void FixedUpdate()
{
    Vector3 moveVector = Vector3.zero;    // 移動速度の入力

    if(isUseCameraDirection)
    {
        Vector3 cameraForward = mainCamera.transform.forward;
        Vector3 cameraRight = mainCamera.transform.right;
        cameraForward.y = 0.0f;    // 水平方向に移動させたい場合はy方向成分を0にする
        cameraRight.y = 0.0f;

        moveVector = moveSpeed * (cameraRight.normalized * _horizontalInput + cameraForward.normalized * _verticalInput);
    } else
    {
        moveVector.x = moveSpeed * _horizontalInput;
        moveVector.z = moveSpeed * _verticalInput;
    }

    _rb.AddForce(moveForceMultiplier * (moveVector - _rb.velocity));
}


すーっと止まるのは

_rb.AddForce(moveForceMultiplier * (moveVector - _rb.velocity));

この行で入力移動速度と現在の移動速度の差に応じて力を加えるからで、
入力がなくなったときには入力移動速度は0になるので、減速する方向に力が加わるって寸法ですね( -v- 。)

参考にmoveForceMultiplierを変化させたときのキャラクターの挙動を示した動画を載せておきます。
値が大きいほどきびきび動きます。
好みに合わせて値を設定するとよさそうです。


UnityでRigidBodyのAddForce()に速度制限をつけてすーっと動かす(その1)

UnityでRigidBodyのAddForce()に速度制限をつけてすーっと動かす(その2)

UnityでRigidBodyのAddForce()に速度制限をつけてすーっと動かす(その3)


以上、Unityのキャラクターの移動方法に関するTipsでした。

Arbor2のUISetTextFromParametersで書式指定してテキスト表示する

ゲームエンジンUnityのステートマシンエディタアセットArbor 2 を使って

UIのテキストに書式指定で値を表示しようとしてちょっと手間取った話です。

 

手っ取り早く方法だけ知りたい方は

「UISetTextFromParametersのBehaviorで書式指定方法はToString()メソッドと同じ」

だけ覚えておけばおっけーです!

(stringの書式指定はstring.formatの{0:F2}とかしか使ったことなかったので時間がかかってしまったのです)

 

Arbor 2はステートマシン(こういう時、こういう条件を満たしたらこれをするというのを図で表したもの)を使ってゲームの挙動が作れる便利アセットです。

作ったステートマシンはUnity Editorでのテスト中にステートの遷移が視覚的にわかるのでうまく使えばバグの少ないゲームづくりができそうです(。・ω・。)

arbor.caitsithware.com

今回の記事で紹介するのはArbor 2のState Behaviorの「UISetTextFromParameters」で書式指定してテキストを表示する方法について。

 

Arbor 2では挙動をコントロールしたいゲームオブジェクトにFSM (Finite State Machine)を追加して、FSMの各ステートでのふるまい (Behavior)を管理します。

利用できるBehaviorは様々あって、必要であればコードを書いて自作もできます。

 

UISetTextFromParametersは名前の通りUIのテキストにArbor 2で管理しているパラメータの値を表示するBehaviorで、プロパティはリンクの通り。

UISetTextFromParameter — Arbor リファレンス 2.1.2 ドキュメント

 

Formatを使えば書式指定できるみたいですが、書式指定の方法についてとくに書いてなくって初心者は詰まってしまいました。

UISetTextFromParametersでどのようにFormatが指定されてるかソースコードを見てみるとParameterで与えた変数のToString()メソッドに引数としてFormatを与えているようです(ソースコードは載せていいのかわからなかったので載せてません)。

 

ToString()メソッドでの書式指定についてはこちらが参考になりました。

tsubakit1.hateblo.jp

 

ということでいまいち締まらないけどこんな感じで記事は終わりです。

書いてるうちにうまくなっていくと信じたいところですね...( -v- 。)