Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8895

様々なシーンで活用できるプレイヤー追従型多機能カメラスクリプト

$
0
0

はじめに

Cinemachineを意地でも使わないyoship1639です。

対象のオブジェクトを舐めまわすように回転・追従する多機能カメラコントローラを作ってみたのでその紹介です。
SceneのMain Cameraにアタッチし追従対象のTransformを指定するだけでいいのでお手軽で、また調整すればプレイヤーの追従にもそのまま使えるので様々なシーンで使えるのではないかと思います。

どの様な機能があるのかご紹介します。

カメラの機能

今回作成したカメラスクリプトは標準入力からの操作でも他スクリプトからでも扱える様にしてあります。

039.png

Target Rotation

ターゲットを中心に周ります。基本機能です。
マウス左+移動です。

Following

ターゲットを中心にしているのでそのまま追従します。

006.gif

Damping

遅延です。Dampingの値を大きくするとカメラが遅延して追従します。
滑らかに見せるには必須の手法です。

001.gif

Free Look

ターゲットを中心としてその周りを見渡すことができます。
特定の場所を見たい場合に使えます。
マウス右+移動です。マウス中央で初期化します。

002.gif

Distance

ターゲットとの距離を変える事が出来ます。これもなくてはならない基本機能ですね。
マウスホイールです。

Look Height

一々プレイヤーの目の位置にトランスフォーム用のゲームオブジェクトを設定するのが面倒な人のための機能です。
見る高さを変えます。

Noise

手ブレです。いかにも人が手で撮っている様に見せます。
あまり大きくすると酔います。

003.gif

Dolly Zoom

めまいショットともいわれるドリーズーム。演出を凝るためにはこの効果を試してみるのもいいかもしれません。

004.gif

Vibration

振動です。縦振動のみ、横振動のみ等も設定できます。

005.gif

Wall Detection

カメラとキャラクタの間に遮蔽物がある場合に正しく処理する機能です。
壁裏にカメラがめり込むのを防ぎます。

Fixed Point

有効にするとカメラの位置をその場から動かさないようにできます。

Update Function

UpdateでシミュレートするかFixedUpdateでシミュレートするかを選べます。
移動対象が剛体の場合はFixedUpdateで追従するのが有効です。

各種アルゴリズム解説

それぞれの機能をどのように実現しているのかを簡単に解説したいと思います。

Target Rotation + Following + Distance + Height

ターゲットを中心にカメラを回転する手法ですが、これはSinCosをうまく組み合わせることで簡単に実現できます。
ターゲットを中心としたカメラの水平位置は、上から見て単位円を描けば簡単に理解できます。ターゲットを後ろから見た状態を基準にした場合単位円のX座標はSin(rot)、Z座標は-Cos(rot)であることが分かるので、まずそれをそのままプログラムに落とし込みます。

[SerializeField]publicfloatrot=0.0f;// 水平回転角度varpos=Vector3.zero;pos.x=Mathf.Sin(rot*Mathf.Deg2Rad);pos.z=-Mathf.Cos(rot*Mathf.Deg2Rad);

次に、Y座標ですが、これは単純でSin(height)です。そして、上下に回り込むほどX,Z座標は小さくなるので、Cos(height)を掛けてあげます。

[SerializeField]publicfloatrot=0.0f;// 水平回転角度[SerializeField]publicfloatheight=0.0f;// 上下回り込み角度varpos=Vector3.zero;pos.x=Mathf.Sin(rot*Mathf.Deg2Rad)*Mathf.Cos(height*Mathf.Deg2Rad);pos.z=-Mathf.Cos(rot*Mathf.Deg2Rad)*Mathf.Cos(height*Mathf.Deg2Rad);pos.y=Mathf.Sin(height*Mathf.Deg2Rad);

単位円の長さは1なので、このままではターゲットからカメラまでの距離が1で固定されてしまうので、distanceを掛けてあげます。また、中心をターゲットにするので、var pos = Vector3.zerovar pos = target.positionに直してあげます。これでターゲットを中心にして回るという処理ができました。ターゲットを中心とするので、自動的にFollowing機能が付いてきます。更に、posにVector3.up * eyeHeightを足してあげれば高さも調整できます。

[SerializeField]publicTransformtarget=null;// 追従ターゲット[SerializeField]publicfloatrot=0.0f;// 水平回転角度[SerializeField]publicfloatheight=0.0f;// 上下回り込み角度[SerializeField]publicfloatdistance=10.0f;// カメラ距離[SerializeField]publicfloateyeHeight=1.0f;// ターゲットの視点の高さvarpos=target.position+Vector3.up*eyeHeight;pos.x+=Mathf.Sin(rot*Mathf.Deg2Rad)*Mathf.Cos(height*Mathf.Deg2Rad)*distance;pos.z+=-Mathf.Cos(rot*Mathf.Deg2Rad)*Mathf.Cos(height*Mathf.Deg2Rad)*distance;pos.y+=Mathf.Sin(height*Mathf.Deg2Rad)*distance;camera.transform.position=pos;camera.transform.LookAt(target.position+Vector3.up*eyeHeight);

これで、4つの機能が出来上がりました。

Damping

遅延はMathf.Lerpを使えば大丈夫です。
先ほどのrotheightの値を遅延させればいいので、このような処理をかませます。

[SerializeField]publicfloatrotationDamping=10.0f;privatefloattargetRot=0.0f;targetRot=Mathf.Lerp(targetRot,rot,Mathf.Clamp01(Time.deltaTime*100.0f/rotationDamping));

Mathf.Lerpのrate値を時間にしてあげる事で、時間がたつにつれてtargetRotrotに近づくようになります。近づく速度はrotationDampingで調整可能です。この処理をheightdistanceにも適用させると動きが滑らかになります。

Free Look

これは意外と簡単で、camera.transform.LookAtの後にcamera.transform.Rotateで回転させればいいだけです。

[SerializeField]publicVector3freeLookRotation=Vector3.zero;camera.transform.LookAt(target.position+Vector3.up*eyeHeight);camera.transform.Rotate(freeLookRotation);

Noise

手ブレは、パーリンノイズを使います。引数を経過時間にすればいいだけなので、これも簡単に作れてしまいます。
Free Lookと同じ様にcamera.transform.LookAtの後にcamera.transform.Rotateを行います。
Zだけ別の変数にしているのは、cameraのupの変動が大きいと見ずらくなってしまうので個別に調整できるようにするためです。

[SerializeField]publicfloatnoise=1.0f;[SerializeField]publicfloatnoiseZ=0.4f;[SerializeField]publicfloatnoiseSpeed=1.0f;varrotNoise=Vector3.zero;rotNoise.x=(Mathf.PerlinNoise(Time.time*noiseSpeed,0.0f)-0.5f)*noise;rotNoise.y=(Mathf.PerlinNoise(Time.time*noiseSpeed,0.4f)-0.5f)*noise;rotNoise.z=(Mathf.PerlinNoise(Time.time*noiseSpeed,0.8f)-0.5f)*noiseZ;camera.transform.Rotate(rotNoise);

Vibration

手ブレと要領でcamera.transform.Rotateしてあげます。
引数をランダムにすればいいだけです。

[SerializeField]publicVector3vibration=Vector3.zero;varvib=Vector3.zero;vib.x=newVector3(Random.Range(-1.0f,1.0f)*vibration.x;vib.y=newVector3(Random.Range(-1.0f,1.0f)*vibration.y;vib.z=newVector3(Random.Range(-1.0f,1.0f)*vibration.z;camera.transform.Rotate(vib);

Dolly Zoom

これはちょっと考えなければできない処理です。ドリーズームはカメラのField of ViewとDistanceをうまく調整する事でターゲットの大きさを変えずに周りの遠近感を変える手法です。

説明が面倒なので抜粋ソースコードを見て納得してください(投げやり)
最初の奴のdistancedollyDistに置き換えればおkです。

[SerializeField]publicfloatdolly=0.34f;privatefloatGetDollyDistance(floatfov,floatdistance){returndistance/(2.0f*Mathf.Tan(fov*0.5f*Mathf.Deg2Rad));}privatefloatGetDollyFoV(floatdolly,floatdistance){return2.0f*Mathf.Atan(distance*0.5f/dolly)*Mathf.Rad2Deg;}vardollyFoV=GetDollyFoV(Mathf.Pow(1.0f/dolly,2.0f),distance);vardollyDist=GetDollyDistance(dollyFoV,distance);camera.fieldOfView=dollyFoV;

一応Unity公式リファレンスにも説明があります。
https://docs.unity3d.com/jp/460/Manual/DollyZoom.html

Wall Detection

壁を検知し壁に埋まらない様にする手法です。ターゲットからカメラ方向にRayを飛ばして壁があったらdistanceをその壁までの距離に置き換えます。

[SerializeField]publicfloatwallDetectionDistance=0.3f;RaycastHithit;vardir=(target.position-camera.transform.position).normalized;if(Physics.SphereCast(pos,wallDetectionDistance,dir,outhit,dollyDist)){dollyDist=hit.distance;}

これだけで壁にめり込まなくなります。

残りのFixed PointとUpdate Functionは特に説明することはないので割愛です。

ソースコード

前章のアルゴリズムをごった煮して入力を受け付ける様にしたら多機能カメラコントローラの完成です。
コピペしてメインカメラに追加しターゲットを設定すればそのまま使えます。

MultifunctionFollowingCamera.cs
usingUnityEngine;publicclassMultifunctionFollowingCamera:MonoBehaviour{[SerializeField]publicTransformtarget;[SerializeField]publicboolenableInput=true;[SerializeField]publicboolsimulateFixedUpdate=false;[SerializeField]publicboolenableDollyZoom=true;[SerializeField]publicboolenableWallDetection=true;[SerializeField]publicboolenableFixedPoint=false;[SerializeField]publicfloatinputSpeed=4.0f;[SerializeField]publicVector3freeLookRotation;[SerializeField]publicfloatheight;[SerializeField]publicfloatdistance=8.0f;[SerializeField]publicVector3rotation;[SerializeField][Range(0.01f,100.0f)]publicfloatpositionDamping=16.0f;[SerializeField][Range(0.01f,100.0f)]publicfloatrotationDamping=16.0f;[SerializeField][Range(0.1f,0.99f)]publicfloatdolly=0.34f;[SerializeField]publicfloatnoise=0.0f;[SerializeField]publicfloatnoiseZ=0.0f;[SerializeField]publicfloatnoiseSpeed=1.0f;[SerializeField]publicVector3vibration=Vector3.zero;[SerializeField]publicfloatwallDetectionDistance=0.3f;[SerializeField]publicLayerMaskwallDetectionMask=1;privateCameracam;privatefloattargetDistance;privateVector3targetPosition;privateVector3targetRotation;privateVector3targetFree;privatefloattargetHeight;privatefloattargetDolly;voidStart(){cam=GetComponent<Camera>();targetDistance=distance;targetRotation=rotation;targetFree=freeLookRotation;targetHeight=height;targetDolly=dolly;vardollyDist=targetDistance;if(enableDollyZoom){vardollyFoV=GetDollyFoV(Mathf.Pow(1.0f/targetDolly,2.0f),targetDistance);dollyDist=GetDollyDistance(dollyFoV,targetDistance);cam.fieldOfView=dollyFoV;}if(target==null)return;varpos=target.position+Vector3.up*targetHeight;varoffset=Vector3.zero;offset.x+=Mathf.Sin(targetRotation.y*Mathf.Deg2Rad)*Mathf.Cos(targetRotation.x*Mathf.Deg2Rad)*dollyDist;offset.z+=-Mathf.Cos(targetRotation.y*Mathf.Deg2Rad)*Mathf.Cos(targetRotation.x*Mathf.Deg2Rad)*dollyDist;offset.y+=Mathf.Sin(targetRotation.x*Mathf.Deg2Rad)*dollyDist;targetPosition=pos+offset;}voidUpdate(){if(!simulateFixedUpdate)Simulate(Time.deltaTime);}voidFixedUpdate(){if(simulateFixedUpdate)Simulate(Time.fixedDeltaTime);}privatevoidSimulate(floatdeltaTime){if(enableInput){if(Input.GetKey(KeyCode.LeftAlt)){dolly+=Input.GetAxis("Mouse ScrollWheel")*0.2f;dolly=Mathf.Clamp(dolly,0.1f,0.99f);}else{distance*=1.0f-Input.GetAxis("Mouse ScrollWheel");distance=Mathf.Clamp(distance,0.01f,1000.0f);}if(Input.GetMouseButton(0)){rotation.x-=Input.GetAxis("Mouse Y")*inputSpeed;rotation.x=Mathf.Clamp(rotation.x,-89.9f,89.9f);rotation.y-=Input.GetAxis("Mouse X")*inputSpeed;}if(Input.GetMouseButton(1)){freeLookRotation.x-=Input.GetAxis("Mouse Y")*inputSpeed*0.2f;freeLookRotation.y+=Input.GetAxis("Mouse X")*inputSpeed*0.2f;}if(Input.GetMouseButtonDown(2)){freeLookRotation=Vector3.zero;}}varposDampRate=Mathf.Clamp01(deltaTime*100.0f/positionDamping);varrotDampRate=Mathf.Clamp01(deltaTime*100.0f/rotationDamping);targetDistance=Mathf.Lerp(targetDistance,distance,posDampRate);targetRotation=Vector3.Lerp(targetRotation,rotation,rotDampRate);targetFree=Vector3.Lerp(targetFree,freeLookRotation,rotDampRate);targetHeight=Mathf.Lerp(targetHeight,height,posDampRate);targetDolly=Mathf.Lerp(targetDolly,dolly,posDampRate);if(Mathf.Abs(targetDolly-dolly)>0.005f){targetDistance=distance;}vardollyDist=targetDistance;if(enableDollyZoom){vardollyFoV=GetDollyFoV(Mathf.Pow(1.0f/targetDolly,2.0f),targetDistance);dollyDist=GetDollyDistance(dollyFoV,targetDistance);cam.fieldOfView=dollyFoV;}if(target==null)return;varpos=target.position+Vector3.up*targetHeight;if(enableWallDetection){RaycastHithit;vardir=(targetPosition-pos).normalized;if(Physics.SphereCast(pos,wallDetectionDistance,dir,outhit,dollyDist,wallDetectionMask)){dollyDist=hit.distance;}}varoffset=Vector3.zero;offset.x+=Mathf.Sin(targetRotation.y*Mathf.Deg2Rad)*Mathf.Cos(targetRotation.x*Mathf.Deg2Rad)*dollyDist;offset.z+=-Mathf.Cos(targetRotation.y*Mathf.Deg2Rad)*Mathf.Cos(targetRotation.x*Mathf.Deg2Rad)*dollyDist;offset.y+=Mathf.Sin(targetRotation.x*Mathf.Deg2Rad)*dollyDist;if(Mathf.Abs(targetDolly-dolly)>0.005f){targetPosition=offset+pos;}else{targetPosition=Vector3.Lerp(targetPosition,offset+pos,posDampRate);}if(!enableFixedPoint)cam.transform.position=targetPosition;cam.transform.LookAt(pos,Quaternion.Euler(0.0f,0.0f,targetRotation.z)*Vector3.up);cam.transform.Rotate(targetFree);if(noise>0.0f||noiseZ>0.0f){varrotNoise=Vector3.zero;rotNoise.x=(Mathf.PerlinNoise(Time.time*noiseSpeed,0.0f)-0.5f)*noise;rotNoise.y=(Mathf.PerlinNoise(Time.time*noiseSpeed,0.4f)-0.5f)*noise;rotNoise.z=(Mathf.PerlinNoise(Time.time*noiseSpeed,0.8f)-0.5f)*noiseZ;cam.transform.Rotate(rotNoise);}if(vibration.sqrMagnitude>0.0f){cam.transform.Rotate(newVector3(Random.Range(-1.0f,1.0f)*vibration.x,Random.Range(-1.0f,1.0f)*vibration.y,Random.Range(-1.0f,1.0f)*vibration.z));}}privatefloatGetDollyDistance(floatfov,floatdistance){returndistance/(2.0f*Mathf.Tan(fov*0.5f*Mathf.Deg2Rad));}privatefloatGetFrustomHeight(floatdistance,floatfov){return2.0f*distance*Mathf.Tan(fov*0.5f*Mathf.Deg2Rad);}privatefloatGetDollyFoV(floatdolly,floatdistance){return2.0f*Mathf.Atan(distance*0.5f/dolly)*Mathf.Rad2Deg;}}

おわりに

以前もカメラワークに関する記事を書かせていただきました。
ゲームの質を劇的に上げるカメラワークの3つの手法解説【減衰・FoV・手ブレ】

3D関連はカメラワークを変えるだけで見た目が大きく変わるのでぜひ参考にしていただければと思います。

この機能が足りない、この挙動がおかしい等ありましたら遠慮なくコメントください!修正バージョンを記載します。
よきUnityライフを。

※本記事で使用しているモデルは以下のライセンスで提供されています。
© Unity Technologies Japan/UCL


Viewing all articles
Browse latest Browse all 8895

Trending Articles