Unity용 하향식 플레이어 컨트롤러 튜토리얼
많은 사람들이 FPS(1인칭 슈팅 게임), RTS(실시간 전략 게임) 등의 게임 장르에 익숙하지만, 특정 카테고리에만 속하지 않는 게임도 많습니다. 그러한 게임 중 하나가 탑다운 슈터(Top-Down Shooter)입니다.
Top-Down Shooter는 플레이어가 위에서 내려다보는 관점에서 제어되는 게임입니다.
하향식 슈팅 게임의 예로는 Hotline Miami, Hotline Miami 2, Original Grand Theft Auto 등이 있습니다.
Unity에서 하향식 문자 컨트롤러를 만들려면 아래 단계를 따르세요.
1단계: 스크립트 생성
이 튜토리얼에서는 하나의 스크립트만 필요합니다.
- 새 스크립트를 생성하고 이름을 SC_TopDownController로 지정한 다음 모든 내용을 제거하고 아래 코드를 그 안에 붙여넣습니다.
SC_TopDownController.cs
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(CapsuleCollider))]
public class SC_TopDownController : MonoBehaviour
{
//Player Camera variables
public enum CameraDirection { x, z }
public CameraDirection cameraDirection = CameraDirection.x;
public float cameraHeight = 20f;
public float cameraDistance = 7f;
public Camera playerCamera;
public GameObject targetIndicatorPrefab;
//Player Controller variables
public float speed = 5.0f;
public float gravity = 14.0f;
public float maxVelocityChange = 10.0f;
public bool canJump = true;
public float jumpHeight = 2.0f;
//Private variables
bool grounded = false;
Rigidbody r;
GameObject targetObject;
//Mouse cursor Camera offset effect
Vector2 playerPosOnScreen;
Vector2 cursorPosition;
Vector2 offsetVector;
//Plane that represents imaginary floor that will be used to calculate Aim target position
Plane surfacePlane = new Plane();
void Awake()
{
r = GetComponent<Rigidbody>();
r.freezeRotation = true;
r.useGravity = false;
//Instantiate aim target prefab
if (targetIndicatorPrefab)
{
targetObject = Instantiate(targetIndicatorPrefab, Vector3.zero, Quaternion.identity) as GameObject;
}
//Hide the cursor
Cursor.visible = false;
}
void FixedUpdate()
{
//Setup camera offset
Vector3 cameraOffset = Vector3.zero;
if (cameraDirection == CameraDirection.x)
{
cameraOffset = new Vector3(cameraDistance, cameraHeight, 0);
}
else if (cameraDirection == CameraDirection.z)
{
cameraOffset = new Vector3(0, cameraHeight, cameraDistance);
}
if (grounded)
{
Vector3 targetVelocity = Vector3.zero;
// Calculate how fast we should be moving
if (cameraDirection == CameraDirection.x)
{
targetVelocity = new Vector3(Input.GetAxis("Vertical") * (cameraDistance >= 0 ? -1 : 1), 0, Input.GetAxis("Horizontal") * (cameraDistance >= 0 ? 1 : -1));
}
else if (cameraDirection == CameraDirection.z)
{
targetVelocity = new Vector3(Input.GetAxis("Horizontal") * (cameraDistance >= 0 ? -1 : 1), 0, Input.GetAxis("Vertical") * (cameraDistance >= 0 ? -1 : 1));
}
targetVelocity *= speed;
// Apply a force that attempts to reach our target velocity
Vector3 velocity = r.velocity;
Vector3 velocityChange = (targetVelocity - velocity);
velocityChange.x = Mathf.Clamp(velocityChange.x, -maxVelocityChange, maxVelocityChange);
velocityChange.z = Mathf.Clamp(velocityChange.z, -maxVelocityChange, maxVelocityChange);
velocityChange.y = 0;
r.AddForce(velocityChange, ForceMode.VelocityChange);
// Jump
if (canJump && Input.GetButton("Jump"))
{
r.velocity = new Vector3(velocity.x, CalculateJumpVerticalSpeed(), velocity.z);
}
}
// We apply gravity manually for more tuning control
r.AddForce(new Vector3(0, -gravity * r.mass, 0));
grounded = false;
//Mouse cursor offset effect
playerPosOnScreen = playerCamera.WorldToViewportPoint(transform.position);
cursorPosition = playerCamera.ScreenToViewportPoint(Input.mousePosition);
offsetVector = cursorPosition - playerPosOnScreen;
//Camera follow
playerCamera.transform.position = Vector3.Lerp(playerCamera.transform.position, transform.position + cameraOffset, Time.deltaTime * 7.4f);
playerCamera.transform.LookAt(transform.position + new Vector3(-offsetVector.y * 2, 0, offsetVector.x * 2));
//Aim target position and rotation
targetObject.transform.position = GetAimTargetPos();
targetObject.transform.LookAt(new Vector3(transform.position.x, targetObject.transform.position.y, transform.position.z));
//Player rotation
transform.LookAt(new Vector3(targetObject.transform.position.x, transform.position.y, targetObject.transform.position.z));
}
Vector3 GetAimTargetPos()
{
//Update surface plane
surfacePlane.SetNormalAndPosition(Vector3.up, transform.position);
//Create a ray from the Mouse click position
Ray ray = playerCamera.ScreenPointToRay(Input.mousePosition);
//Initialise the enter variable
float enter = 0.0f;
if (surfacePlane.Raycast(ray, out enter))
{
//Get the point that is clicked
Vector3 hitPoint = ray.GetPoint(enter);
//Move your cube GameObject to the point where you clicked
return hitPoint;
}
//No raycast hit, hide the aim target by moving it far away
return new Vector3(-5000, -5000, -5000);
}
void OnCollisionStay()
{
grounded = true;
}
float CalculateJumpVerticalSpeed()
{
// From the jump height and gravity we deduce the upwards speed
// for the character to reach at the apex.
return Mathf.Sqrt(2 * jumpHeight * gravity);
}
}
2단계: 셰이더 만들기
이 튜토리얼에는 또한 Aim 대상이 나머지 개체(항상 맨 위에) 오버레이되도록 만드는 데 필요한 사용자 정의 셰이더가 필요합니다.
- 프로젝트 뷰 -> 생성 -> 셰이더 -> 표준 표면 셰이더를 마우스 오른쪽 버튼으로 클릭합니다.
- 셰이더 이름 지정 "Cursor"
- 셰이더를 열고 그 안의 모든 항목을 제거한 후 아래 코드를 붙여넣습니다.
커서.쉐이더
Shader "Custom/FX/Cursor" {
Properties {
_MainTex ("Base", 2D) = "white" {}
}
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_ST;
struct v2f {
half4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
v2f vert(appdata_full v) {
v2f o;
o.pos = UnityObjectToClipPos (v.vertex);
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag( v2f i ) : COLOR {
return tex2D (_MainTex, i.uv.xy);
}
ENDCG
SubShader {
Tags { "RenderType" = "Transparent" "Queue" = "Transparent+100"}
Cull Off
Lighting Off
ZWrite Off
ZTest Always
Fog { Mode Off }
Blend SrcAlpha OneMinusSrcAlpha
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
}
FallBack Off
}
3단계: 하향식 캐릭터 컨트롤러 설정
하향식 캐릭터 컨트롤러를 설정해 보겠습니다.
- 새로운 GameObject를 생성하고 호출합니다. "Player"
- 새 큐브를 만들고 크기를 조정합니다. (제 경우에는 크기가 (1, 2, 1)입니다.)
- 두 번째 큐브를 만들고 크기를 훨씬 더 작게 조정한 다음 위쪽 영역으로 이동합니다. (단순히 플레이어가 어느 방향을 보고 있는지 알기 위한 것입니다.)
- 두 큐브를 "Player" 개체 내부로 이동하고 BoxCollider 구성 요소를 제거합니다.
이제 더 진행하기 전에 Aim 대상 프리팹을 만들어 보겠습니다.
- 새로운 GameObject를 생성하고 호출합니다. "AimTarget"
- 새 쿼드(GameObject -> 3D 개체 -> 쿼드)를 만들고 "AimTarget" 개체 내부로 이동합니다.
- 아래 텍스처를 쿼드에 할당하고 Material Shader를 다음으로 변경합니다. 'Custom/FX/Cursor'
- "AimTarget"을 Prefab에 저장하고 장면에서 제거합니다.
플레이어 인스턴스로 돌아가기:
- SC_TopDownController 스크립트를 "Player" 개체에 연결합니다(Rigidbody 및 CapsuleCollider와 같은 몇 가지 추가 구성 요소가 추가되었음을 알 수 있습니다).
- 플레이어 모델과 일치할 때까지 CapsuleCollider의 크기를 조정합니다(제 경우에는 Height가 2로 설정되고 Center가 (0, 1, 0)으로 설정됩니다)
- 마지막으로 SC_TopDownController에 "Player Camera" 및 "Target Indicator Prefab" 변수를 할당합니다.
이제 Player 인스턴스가 준비되었습니다. 테스트해 보겠습니다.
모든 것이 예상대로 작동합니다.