2013/10/18(金) [n年前の日記]
#1 [unity] 先日作った横スクロールアクションのテストサンプルを修正中
_先日作ったテストサンプル
のあちこちを修正中。
◎ 角にめり込むバグを修正 :
天井と壁の角にめり込んでその後怪しい動作になってしまうバグを修正できた、ような気がする。以下のような状態になってたみたいで。
とりあえず頭のあたりでも補正するようにして、かつ、「天井があるか?」→「壁があるか?」の順番でモードを変えるようにしてみたり。
一応、角にめり込まなくなったように見えるけど…。まだどこか見落としがありそうな…。
本来であれば、当たった地形ポリゴンの法線ベクトルの向きを調べて、壁か天井か判定したほうがいいような気もする。そういう処理にしておかないと、例えばス○ライダー飛竜のような色々な角度を持った地形に対応できないのではないのかなと。
地形ポリゴンの法線ベクトルは、 _RaycastHit.normal で得られそう。 _ベクトルの内積 を使えば、横向きか下向きかの判定もできそうな予感。
とりあえず頭のあたりでも補正するようにして、かつ、「天井があるか?」→「壁があるか?」の順番でモードを変えるようにしてみたり。

一応、角にめり込まなくなったように見えるけど…。まだどこか見落としがありそうな…。
本来であれば、当たった地形ポリゴンの法線ベクトルの向きを調べて、壁か天井か判定したほうがいいような気もする。そういう処理にしておかないと、例えばス○ライダー飛竜のような色々な角度を持った地形に対応できないのではないのかなと。
地形ポリゴンの法線ベクトルは、 _RaycastHit.normal で得られそう。 _ベクトルの内積 を使えば、横向きか下向きかの判定もできそうな予感。
◎ ジャンプ関係のバグを修正。 :
足場から横に移動してポトリと落ちてる最中、空中でジャンプが2回できてしまうバグがあったので修正。地面の上でジャンプしているなら1回目のジャンプ開始として扱って、空中でジャンプしているなら2段ジャンプ開始として扱うことに。
壁つかまりや天井ぶら下がり状態から下に落ち始めた直後にすぐさま空中でジャンプ(=2段ジャンプ)すると地形を突き抜けてしまうバグも修正。ジャンプ開始時に天井とぶつかってないかチェックしてぶつかってたら補正する、みたいな処理を入れてみたり。
細かいところで色々出てくるなと…。もしかすると、モードをもっと細かく分けたほうが ―― いわゆるステップ派のノリで書いたほうがいいのかもしれない。ポトリと落ちてる最中のモードとか、天井ぶら下がりから下に落ちてる最中のモードとか。似たような処理がずらずら並ぶことになるだろうけど、モード毎に対処していけばいいからバグは少なくなりそうな。今やってる書き方は、たぶんフラグ派に近いのではと。
壁つかまりや天井ぶら下がり状態から下に落ち始めた直後にすぐさま空中でジャンプ(=2段ジャンプ)すると地形を突き抜けてしまうバグも修正。ジャンプ開始時に天井とぶつかってないかチェックしてぶつかってたら補正する、みたいな処理を入れてみたり。
細かいところで色々出てくるなと…。もしかすると、モードをもっと細かく分けたほうが ―― いわゆるステップ派のノリで書いたほうがいいのかもしれない。ポトリと落ちてる最中のモードとか、天井ぶら下がりから下に落ちてる最中のモードとか。似たような処理がずらずら並ぶことになるだろうけど、モード毎に対処していけばいいからバグは少なくなりそうな。今やってる書き方は、たぶんフラグ派に近いのではと。
◎ Normal Mapを使ってみた。 :
Unity は Normal Map とやらを使えるらしいと知り、試しに貼ってみたり。Normal Map を使うと、表面に凹凸があるように見せかけることができるそうで。
_Unity - Unity Manual
白黒の画像(グレー画像)を用意して、白が高い部分、黒が低い部分で描いといて。Unityにインポートした際に、Texture Type を Normal Map にしておく。 Create from Grayscale にチェックが入っていれば、白黒画像からNormal Map用の画像を生成してくれるっぽい。

後は、Normal Map を使いたいマテリアルの Shader を Bumped Diffuse にして、NormalMap に先ほど設定したNormalMap用テクスチャを指定する。
たしかに見た目、凸凹してるようになった。イイ感じ。
ただ、問題も。きっちり真面目にUVを指定してない場所がハッキリ分かるようになったような。手抜きモデルで Normal Map を使うと手抜きぶりが更にバレバレになるのだなと…。
_Unity - Unity Manual
白黒の画像(グレー画像)を用意して、白が高い部分、黒が低い部分で描いといて。Unityにインポートした際に、Texture Type を Normal Map にしておく。 Create from Grayscale にチェックが入っていれば、白黒画像からNormal Map用の画像を生成してくれるっぽい。

後は、Normal Map を使いたいマテリアルの Shader を Bumped Diffuse にして、NormalMap に先ほど設定したNormalMap用テクスチャを指定する。
たしかに見た目、凸凹してるようになった。イイ感じ。
ただ、問題も。きっちり真面目にUVを指定してない場所がハッキリ分かるようになったような。手抜きモデルで Normal Map を使うと手抜きぶりが更にバレバレになるのだなと…。
◎ 一応ソースも貼ってみたり。 :
無駄に長い上にゲロ吐きそうな汚いソースだろうけど、Unity上でこういう感じに書いたらこういう操作仕様も一応作れる、てな実例の一つ、ということで…。
Player.cs
このソースをくっつけたオブジェクトの、Inspector上の設定は…。
地形モデルは…。
以下が、地形アタリ用モデル。タグ名 = Ground。
以下が、通行止め用モデル。タグ名 = Wall。
Player.cs
using UnityEngine;
using System.Collections;
using System.Linq;
public class Player : MonoBehaviour {
public float Gravity = 120.0f; // 重力加速度
public float Speed = 12; // 横方向移動速度
public float SpeedClamber = 8; // 壁つかまり・天井ぶら下がり時の移動速度
public float JumpSpeed = 45; // ジャンプ開始速度
public float MaxSpeed = 120; // 移動速度の最大値
public float ShortJumpXspeed = 3; // 軽くジャンプする際のx速度
public float ShortJumpYspeed = 25; // 軽くジャンプする際のy速度
public float AdjustX = 0.6f; // アタリ判定用。x方向の距離
public float AdjustY = 1.2f; // アタリ判定用。y方向の距離
public float RealAdjustDiff = 0.01f; // 補正値を微妙に少なくする値
public int GroundLayerNumber = 9; // 地形アタリレイヤー番号
public string GroundTag = "Ground"; // 地形モデルのタグ名
public string WallTag = "Wall"; // 通行止めの壁のタグ名
public bool DUMP_SORT_HITS = false; // 開発用
private Vector3 vec; // 現在の速度
private Vector3 oldPos; // 前フレームの座標
private Vector3 nowFramePos; // 次フレームの座標
private Vector3 adjustPos; // 補正後の座標格納用
private int groundLayer; // 地形アタリを取る際のレイヤーマスク値
private int jumpCount; // 0以外ならジャンプ中
private bool isGrounded; // 足場・地面に立っている
private bool isCeiling; // 天井にぶつかっている
private int sideAdjustRL; // 壁で横方向に補正されたかどうか
private int SideAdjustRLold;
private bool isWallTop; // 通行止めの壁か(天井)
private bool isWallSide; // 通行止めの壁か(横方向)
private bool jumpDown; // 足場から飛び降り中
private Vector3 targetPos; // モード移行時の目標座標
private float moveTimer; // モード移行時用タイマー
private bool hitEnemy; // 敵オブジェクトと当たったかどうかのフラグ
// モードの種類(歩行、壁つかまり、天井ぶら下がり等)
enum Status {
Walk, Clamber, Hanging, StepChangeing
}
private Status step; // モード
private Status nextStep; // 次回のモード値
// 初期化処理
void Start() {
vec = Vector3.zero;
jumpCount = 0;
isGrounded = false;
sideAdjustRL = 0;
isWallTop = false;
isWallSide = false;
jumpDown = false;
groundLayer = 1 << GroundLayerNumber;
hitEnemy = false;
step = Status.Walk;
}
// 毎フレーム呼ばれる処理
void Update() {
float xd = Input.GetAxisRaw("Horizontal"); // 左:-1、右:+1
float yd = Input.GetAxisRaw("Vertical"); // 下:-1、上:+1
nowFramePos = transform.position;
adjustPos = Vector3.zero;
switch (step) {
case Status.Walk:
// 歩行 or ジャンプ状態
Walk(xd, yd);
break;
case Status.Clamber:
// 壁つかまり状態
Clamber(xd, yd);
break;
case Status.Hanging:
// 天井ぶら下がり状態
Hanging(xd, yd);
break;
case Status.StepChangeing:
// ぶら下がり←→壁つかまりへの移行中
nowFramePos += (vec * Time.deltaTime);
moveTimer -= Time.deltaTime;
if (moveTimer <= 0) {
step = nextStep;
nowFramePos = targetPos;
vec = Vector3.zero;
jumpCount = 0;
}
break;
}
Vector3 f = nowFramePos - transform.position;
transform.Translate(f);
}
// 歩行 or ジャンプ中の処理
void Walk(float xd, float yd) {
// 左右キーで移動
if (vec.y == 0 || xd != 0) vec = new Vector3(xd * Speed, vec.y, 0);
// 重力加速度を速度に加える
vec += (Vector3.down * Gravity * Time.deltaTime);
// y方向の最大速度を超えないようにする
if (vec.y < 0 && vec.y < -MaxSpeed) vec.y = -MaxSpeed;
if (vec.y > 0 && vec.y > MaxSpeed) vec.y = MaxSpeed;
// 新しい座標を算出
nowFramePos += vec * Time.deltaTime;
float trueVecX = vec.x;
if (vec.y <= 0) {
// 上に飛んでいないなら、天井とアタリ判定はしない
isCeiling = false;
}
else {
// 天井と補正
isCeiling = AdjustCeiling(nowFramePos, 1.0f);
if (isCeiling) nowFramePos = adjustPos; // 天井があったら実際に補正
if (isWallTop) isCeiling = false; // 通行止めの天井だったら、天井はなかったことにする
}
// 空中の足場から飛び降り中なら、一定距離は地面とのアタリ補正をしない
// アタリ補正をしてしまうと、また空中の足場に補正されてしまうので。
// 上向きに飛んでいる場合も、非飛び降り中にする。
if (jumpDown && ((oldPos.y - nowFramePos.y) > (AdjustY * 2) || vec.y > 0)) jumpDown = false;
int chkFg = (xd > 0) ? 0x02 : ((xd < 0) ? 0x01 : 0x03);
bool sideAdjustEnable = true;
if (!isCeiling && !jumpDown) {
// 頭のあたりのy座標を使って一旦横方向に補正
// これをしないと壁と天井の角に突っ込んでしまう
Vector3 tpos = nowFramePos;
tpos.y += AdjustY;
if (AdjustSide(tpos, 1.0f, chkFg) != 0) {
nowFramePos = adjustPos;
nowFramePos.y -= AdjustY;
if (isWallSide) {
sideAdjustRL = 0;
sideAdjustEnable = false;
}
}
}
// 体の中心y座標で横方向に補正
if (sideAdjustEnable) AdjustSideUseCenter(chkFg);
if (jumpDown) {
isGrounded = false;
} else {
if (vec.y <= 0) {
// 地面の上に居るか、下に向かって落ちている状態
isGrounded = AdjustGround((isGrounded) ? 2.0f : 1.0f); // 地面と補正
}
else {
// 上に向かって飛んでいる状態
isGrounded = false; // 地面とはぶつかってないことにする
}
}
// 壁、キー入力、速度を見て、壁つかまりを要求されてるかをフラグにする
bool reqClamber = false;
if(sideAdjustRL != 0 && !isWallSide) {
// 横に、通行止めではない壁がある
if (yd > 0) reqClamber = true; // 上キーを押された
else {
if ((sideAdjustRL & 0x02) != 0) {
// 右に壁がある
if ((trueVecX > 0 || xd > 0) && (jumpCount > 0 || vec.y != 0)) reqClamber = true;
}
if ((sideAdjustRL & 0x01) != 0) {
// 左に壁がある
if ((trueVecX < 0 || xd < 0) && (jumpCount > 0 || vec.y != 0)) reqClamber = true;
}
}
}
if (isCeiling) {
// 天井にぶつかったので、天井ぶら下がりに移行
step = Status.Hanging;
vec = Vector3.zero;
jumpCount = 0;
}
else if (reqClamber) {
// 壁つかまり状態に移行
step = Status.Clamber;
vec = Vector3.zero;
jumpCount = 0;
SideAdjustRLold = sideAdjustRL;
// 天井と補正
// これをしないと、通行止めの天井に当たって落ちた後の動作がおかしくなる
isCeiling = AdjustCeiling(nowFramePos, 1.0f);
if (isCeiling) nowFramePos = adjustPos;
if (isWallTop) isCeiling = false;
}
else if (Input.GetButtonDown("Jump")) {
// ジャンプキーが押された
if (jumpCount == 0) {
// 未ジャンプ状態
if (!jumpDown && isGrounded && yd < 0 && xd == 0) {
// 下キーも押されてるので、下に降りられるか調べる
RaycastHit[] hits = GetRayCastHits(nowFramePos, Vector3.down, 100.0f);
if (hits.Length >= 2) {
// 床ポリゴンが2つ以上見つかった
if (Mathf.Abs(hits[0].point.y - hits[1].point.y) > 2.0f) {
// 床ポリゴンは一頭身以上離れてるから、この場所は降りられそう
// 下に降りる
oldPos = nowFramePos;
jumpDown = true;
}
}
}
else {
// ジャンプする
vec.y = JumpSpeed;
jumpCount = (isGrounded)? 1 : 2;
}
}
else if (jumpCount == 1) {
// ジャンプ中に再度ジャンプボタンが押されたので二段ジャンプする
vec.y = JumpSpeed * 0.8f;
jumpCount++;
}
}
else if (jumpCount > 0 && isGrounded) {
// ジャンプ中に地面に着いたのでジャンプ終了
jumpCount = 0;
}
}
// 壁つかまり中の処理
void Clamber(float xd, float yd) {
// 上下キーで移動
vec = Vector3.up * yd * SpeedClamber;
nowFramePos += vec * Time.deltaTime;
// 地形モデルとアタリ判定をして補正
AdjustSideUseCenter(SideAdjustRLold);
if (vec.y < 0) {
isGrounded = AdjustGround(1.0f);
isCeiling = false;
isWallTop = false;
}
else if (vec.y > 0) {
isGrounded = false;
isCeiling = AdjustCeiling(nowFramePos, 1.0f);
if (isCeiling) nowFramePos = adjustPos;
if (isWallTop) isCeiling = false;
}
bool rlKeyEnable = false;
if ((SideAdjustRLold & 0x01) != 0 && xd > 0) rlKeyEnable = true;
if ((SideAdjustRLold & 0x02) != 0 && xd < 0) rlKeyEnable = true;
if (sideAdjustRL != 0) {
// 壁がまだある
if (isGrounded && rlKeyEnable) {
// 地面の上で左右キーが押された。歩行モードに移行
step = Status.Walk;
vec.y = 0;
}
else if (isCeiling && rlKeyEnable) {
// 天井の下で左右キーが押された。ぶら下がりモードに移行
step = Status.Hanging;
vec.y = 0;
}
else if (Input.GetButtonDown("Jump")) {
// Jumpボタンが押された。ジャンプする
float dx = ((SideAdjustRLold & 0x02) != 0) ? -1 : 1;
step = Status.Walk;
sideAdjustRL = 0;
jumpCount = 1;
// 下キーも押されていたら、もしくは通行止めの壁なら落ちる。
// そうでなければ、ジャンプする
vec = new Vector3(dx * Speed, (yd < 0 || isWallTop) ? 0 : JumpSpeed, 0);
nowFramePos.x += dx * Speed * Time.deltaTime;
}
}
else {
// 壁が無くなった
float d = ((SideAdjustRLold & 0x01) != 0) ? -1 : 1;
bool ret = AdjustCeiling(nowFramePos + new Vector3(d * 2.0f, -AdjustY, 0), AdjustY * 2.0f);
if (ret && !isWallTop) {
// 天井はあるので、ぶら下がりモードへ移行
SetNextStep(Status.Hanging);
}
else {
// 軽くジャンプ。足場に飛び乗る
step = Status.Walk;
Vector3 dir = ((SideAdjustRLold & 0x02) != 0) ? Vector3.right : Vector3.left;
vec = new Vector3(dir.x * ShortJumpXspeed, ShortJumpYspeed, 0);
jumpCount = 1;
}
}
}
// 天井ぶら下がり中の処理
void Hanging(float xd, float yd) {
vec = Vector3.right * xd * SpeedClamber;
nowFramePos += vec * Time.deltaTime;
isGrounded = false;
isCeiling = AdjustCeiling(nowFramePos, 3.0f);
if (isCeiling) nowFramePos = adjustPos;
if (isWallTop) isCeiling = false;
if (vec.x != 0) AdjustSideUseCenter((vec.x > 0) ? 0x02 : 0x01);
if (isCeiling) {
// 天井がまだある
if (yd < 0) {
// 下キーが押された
if (Input.GetButtonDown("Jump")) {
// Jumpボタンも押された。飛び降りる
step = Status.Walk;
vec.y = 0;
oldPos = nowFramePos;
jumpDown = true;
jumpCount = 1;
}
else if (sideAdjustRL != 0) {
// 壁が横にある。壁つかまりモードに移行
step = Status.Clamber;
vec = Vector3.zero;
jumpCount = 0;
SideAdjustRLold = sideAdjustRL;
}
}
}
else {
// 天井が無くなった
int ret = AdjustSide(nowFramePos + (Vector3.up * 2.0f), 1.0f, 0x03);
if (ret != 0 && !isWallSide) {
// 壁はあるので、壁つかまりモードに移行
SideAdjustRLold = ret;
SetNextStep(Status.Clamber);
}
else {
// 壁すらないので落ちる
step = Status.Walk;
vec.y = 0;
oldPos = nowFramePos;
jumpDown = true;
jumpCount = 1;
}
}
}
// 壁つかまり←→天井ぶら下がりの移行モードを設定
void SetNextStep(Status nextStepValue) {
step = Status.StepChangeing;
nextStep = nextStepValue;
moveTimer = 0.3f;
targetPos = adjustPos;
vec = (adjustPos - nowFramePos) / moveTimer;
jumpCount = 0;
}
// 動作状態表示
void OnGUI() {
string s = "";
switch (step) {
case Status.Walk:
s += " 歩行 ";
break;
case Status.Clamber:
s += " 壁登り ";
break;
case Status.Hanging:
s += " ぶら下がり";
break;
default:
s += " 移行中 ";
break;
}
s += ((isGrounded) ? "↓" : " ");
s += ((isCeiling) ? "↑" : " ");
s += ((sideAdjustRL & 0x01) != 0) ? "←" : " ";
s += ((sideAdjustRL & 0x02) != 0) ? "→" : " ";
GUI.Box(new Rect(10, 10, 400, 22), s);
if (hitEnemy) {
GUI.Box(new Rect(10, 30, 200, 22), "Hit Enemy");
}
}
// 光線を飛ばして、交差した「ポリゴン」群との交点情報を配列で返す。
// オブジェクト内に交差する面が複数あっても、その全てを返す。
RaycastHit[] GetRayCastHits(Vector3 pos, Vector3 vec, float dist) {
Vector3 ofs = vec.normalized;
ArrayList a = new ArrayList();
RaycastHit hit;
while (Physics.Raycast(pos, vec, out hit, dist, groundLayer)) {
a.Add(hit);
pos = hit.point + ofs;
}
if (DUMP_SORT_HITS) {
string s = "";
foreach (RaycastHit h in a) s += h.point.y + " , ";
Debug.Log(s);
}
return (RaycastHit[])a.ToArray(typeof(RaycastHit));
}
// 地面とアタリ判定して補正
bool AdjustGround(float distMul) {
Vector3 pos = nowFramePos;
pos.y = transform.position.y + 0.01f;
float dist = (AdjustY * distMul) + Mathf.Abs(vec.y * Time.deltaTime) + 0.02f;
RaycastHit hit;
if (Physics.Raycast(pos, Vector3.down, out hit, dist, groundLayer)) {
if (vec.y <= 0) {
// 上向きに飛んでない状態なら足元の補正をする
nowFramePos.y = (hit.point.y + AdjustY - RealAdjustDiff);
vec.y = 0;
}
return true;
}
return false;
}
// 天井とアタリ判定して補正
bool AdjustCeiling(Vector3 pos, float distMul) {
bool adjusted = false;
isWallTop = false;
float dist = (AdjustY * distMul) + Mathf.Abs(vec.y * Time.deltaTime) + 0.01f;
RaycastHit hit;
if (Physics.Raycast(pos, Vector3.up, out hit, dist, groundLayer)) {
adjusted = true;
pos.y = (hit.point.y - AdjustY + RealAdjustDiff);
vec.y = 0;
if (hit.transform.gameObject.tag == WallTag) isWallTop = true;
}
adjustPos = pos;
return adjusted;
}
// 横方向で地形モデルとアタリ判定して補正
// 返り値 : bit 0 on = 左側で補正、bit 1 on = 右側で補正
// adjustPos に補正後の座標を格納して戻る。
// 通行止めの壁にぶつかった際は isWall を true にする。
int AdjustSide(Vector3 pos, float distMul, int CheckRL) {
int adjusted = 0;
isWallSide = false;
float av = AdjustX * distMul;
RaycastHit hit;
if ((CheckRL & 0x02) != 0 && Physics.Raycast(pos, Vector3.right, out hit, av, groundLayer)) {
pos.x = (hit.point.x - AdjustX + RealAdjustDiff);
if (vec.x > 0) vec.x = 0;
adjusted |= 0x02;
if (hit.transform.gameObject.tag == WallTag) isWallSide = true;
}
if ((CheckRL & 0x01) != 0 && Physics.Raycast(pos, Vector3.left, out hit, av, groundLayer)) {
pos.x = (hit.point.x + AdjustX - RealAdjustDiff);
if (vec.x < 0) vec.x = 0;
adjusted |= 0x01;
if (hit.transform.gameObject.tag == WallTag) isWallSide = true;
}
adjustPos = pos;
return adjusted;
}
// 体の中心y座標で横方向に補正
void AdjustSideUseCenter(int chkFg) {
sideAdjustRL = AdjustSide(nowFramePos, 1.0f, chkFg);
if (sideAdjustRL != 0) nowFramePos = adjustPos; // 壁があったら実際に補正
if (isWallSide) sideAdjustRL = 0; // 通行止めの壁なら、壁は無かったことにする
}
// 敵その他との衝突判定
void OnTriggerEnter(Collider other) {
if (other.gameObject.tag == "Enemy") hitEnemy = true;
}
void OnTriggerExit(Collider other) {
if (other.gameObject.tag == "Enemy") hitEnemy = false;
}
}
Public Domainってことで。どうせこのままではまだまだゲームにならないから改造・改良が必須だし…。このソースをくっつけたオブジェクトの、Inspector上の設定は…。
- ○○○ Collider の Is Trigger は有効。
- Rigidbody の Use Gravity は無効。Is Kinematic は有効。
地形モデルは…。
- 地形アタリ判定用レイヤーに「Ground」というレイヤー名を設定。今回は9番レイヤーにした。
- プレイヤーキャラと地形アタリを取るオブジェクトは、地形アタリ用モデルと、通行止め用モデルの2つだけにした。その2つに、Groundレイヤーを指定。
- 地形アタリ用モデルには「Ground」というタグ名を、通行止め用モデルには「Wall」というタグ名を指定。
- プレイヤーキャラは、z=0 で、xy平面上を移動。
以下が、地形アタリ用モデル。タグ名 = Ground。
以下が、通行止め用モデル。タグ名 = Wall。
[ ツッコむ ]
以上、1 日分です。



