(유니티3D)CharacterController 횡스크롤 캐릭터
Tree-Sha 게임의 캐릭터가 지니고 있는 스크립트에 대해
이 게임은 빙글빙글 돌고 3D 환경이긴 하지만 어쨌든 기본 이동은 2D 횡스크롤 방식을 사용하고 있습니다.
실질적으로 유저가 느끼는 것은 좌우 이동과 점프와 낙하 밖에 없는데요.
여기서는 이 캐릭터가 어떤 코드를 지니고 있어서 어떻게 움직이는지에 대해 적겠습니다.
Rigidbody 컴포넌트를 사용하고 있지 않기에 직접 점프, 중력, 관성 등 필요한건 수제작 해야하기에 다소 복잡할 수 있습니다.
설명에 앞서 이 코드는 실제로 Tree-Sha 게임 캐릭터가 쓰는 코드랑은 다릅니다.
이전 글들과 마찬가지로 최대한 정리하고 주석을 달아놓은 것도 있지만 다른 기믹과의 상호작용 요소들을 대부분 뺐습니다.
슬라이드. 이 게임에선 마법이라는 설정의 요소 또한 뺐습니다.
가능하면 이 캐릭터 자체만을 설명하고 싶었기 때문이기도 하고 다른 기믹들을 모두 올리지는 못 할 것이라 생각했기 때문입니다.
또한 스크립트명도 'Player'에서 'PlayerRepair'로 바꿨기 때문에 아래글의 움직이는 땅 코드와 연결하려면 스크립트명을 바꿀 필요가 있습니다. 둘 중 아무거나 상관 없습니다.
그리고 이 캐릭터가 지닌 사운드(발소리 등), 애니매이션, 모델링 등은 제가 만든 것이 아니기 때문에 여기에 올릴 수 없는 점 양해 바랍니다.
결론적으로 이 코드만으론 실제로 적용하면 수많은 에러를 내뿜을 것입니다. 모델링도, 애니매이션도, 소리도 없으니까요.
아마 아래에 올릴 Heirarchy 형태를 맞추고 없는 부분은 전부 삭제하면 움직임 관련은 문제 없이 될 수도 있습니다만
꽤 오랫동안 CharacterController에 대해 공부해봤지만 대부분을 직접 구현해야 하기에 애초에 호환성을 염두하고 코드를 짜지 않으면 다용도 활용은 어려울 거 같습니다.
하지만 실제로 이 코드를 그대로 쓰실 분은 없다고 생각합니다.
만약 이 코드를 사용할 일이 있다면 그대로 복사하지 마시고 필요한 부분이 어떻게 생겨서 작동하는지만 봐주시면 감사하겠습니다.
캐릭터입니다.
GroundTriggerCheck는 왼쪽에 보면 발 부분에 있는 구형 콜라이더입니다.
움직이는 땅이나 미끄러운 이끼 등에 올라 탔는지를 확인하는 용도입니다.
PlayerModel은 캐릭터의 모델링과 애니매이터 컴포넌트가 담겨있습니다.
CameraPivot은 기존에 쓰던 카메라는 다른 인원이 만든 것이기에 임시로 사용하기 위해 만든 것입니다.
이 피봇은 보스전 때 카메라를 반대로 돌리려고 놔둔 것입니다.
카메라 내부엔 버튼들이 있는데 저 버튼들은 Canvas를 통한 것이 아닌 카메라에 직접 붙어있는 스티커 같은 녀석입니다.
UGUI 버튼은 눌렀다 때야 인식이 이루어져서 사용하기 어려워 저렇게 카메라에 붙인 다음 콜라이더를 붙이고 레이를 쏘는 방식을 적용하였습니다.
UGUI로도 이와 같은 기능을 구현 할 수 있습니다. 이 당시에는 몰라서 이렇게 했는데 이럴 필요가 없었습니다..
이런식으로 버튼들이 카메라에 붙어 공중에 떠있습니다.
다음은 인스펙터 창입니다.
최대한 직관적으로 이름들을 지었습니다.
중력의 경우 기본은 9.81입니다만 실제로 게임에 적용하니 다들 붕 뜨는 느낌이라더군요.
아마 게임이란게 전반적으로 템포가 빠른 편인데 중력만 현실 고증 반영하니 느리게 느껴지는 모양입니다.
그래서 중력을 다소 높이고 그에 맞춰 점프력도 늘려줍니다.
Cirlce Move의 경우 키면 위의 센터를 기준으로 반지름 거리만큼을 돕니다.
그래서 특정 구간에만 임시로 센터의 위치를 바꾸고 반지름을 줄이면
이런 연출을 만들 수 있습니다.
isBoss의 경우 원래는 보스씬 전용이라 저런 이름으로 해뒀는데 true가 되면 카메라와 이동 방향이 반전 됩니다.
맵은 임시로 만든겁니다. 불은.. 신경 안 쓰셔도 됩니다.
소리는 원하는 소리를 넣으면 됩니다.
현재 들어있는 소리는 발 간격에 맞춰 소리가 나도록 사운드가 설정되어있습니다.
참고로 일반 뛰는 소리, 박스 밀 때 소리, 점프 소리 전부 사실 동일합니다.
원래 뛰는 소리만 줬는데 박스 밀 때 어색해서 제가 임의로 그 때만 0.75배로 재생되게 했더니 딱 좋아서 쓰기로 했고 점프 또한 1.5배로 재생시켰더니 딱 좋아서 쓰기로 했습니다. 다만 점프의 경우 즉시 한 번만 소리나야 하기에 사운드를 잘라서 따로 만들었습니다.
일단 인스펙터 창으로 알 수 있는 부분은 이정도이니 다음은 코드로 설명하겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerRepair : MonoBehaviour { [Tooltip("이동속도")] public float moveSpeed = 5.0f; [Tooltip("점프력")] public float jumpPower = 5.0f; [Tooltip("중력 가속도")] public float gravity = 9.81f; [Tooltip("가속도 ")] public float accel = 1.0f; [Tooltip("공중 제어 영향력(1이면 지상과 동일)")] public float airControl = 0.2f; [Tooltip("호를 그리며 돌때의 중심")] public Transform radiusCenter; [Tooltip("호를 그리며 돌때 중심으로부터의 거리")] public float radius = 10.0f; [Tooltip("호를 그리며 이동할 것인지")] public bool circleMove = true; [Tooltip("보스씬일때. 이동과 카메라 위치를 반대로 적용시킨다.")] public bool isBoss = false; //HideInInspector를 해둔 변수들은 다른 스크립트에서도 영향력을 행사할 수 있게 하기 위함에 있다. [HideInInspector] public GameObject model; //모델링이 담긴 오브젝트 [HideInInspector] public float moveValue = 0; //실제로 좌우로 움직일 값 [HideInInspector] public float nowMoveX = 0.0f;//현재 좌우 이동 속도 [HideInInspector] public float nowMoveY = 0.0f;//현재 낙하 속도 [HideInInspector] public Vector3 moveVector; //최종 연산되어 Move 에 들어갈 값 Transform respawnPos; //Awake 시 스폰될 위치 bool lineMove = false; //직선 이동을 시작하고 첫 프레임인지 Vector3 lineStartPosition; //직선 이동을 시작한 좌표 int tCount; //터치 카운트 bool die = false; //죽은 상태 Transform cameraPivot; GameObject leftButton; //모바일을 위한 버튼들 GameObject rightButton; GameObject jumpButton; GameObject actionButton; ImageChange leftButtonChange; //버튼을 눌렀을 때 이미지를 변경하기 위한 스크립트 ImageChange rightButtonChange; ImageChange jumpButtonChange; ImageChange actionButtonChange; //Fairy fairy; //슬라이드 기능. 원래는 요정이라는 설정이었는데 마법으로 바뀌었다. int rayMask; //슬라이드 및 모바일 버튼용 마스크 bool leftButtonOn = false; //이번 프레임에 버튼들이 눌렸는지 bool rightButtonOn = false; bool jumpButtonOn = false; bool actionButtonOn = false; bool jumpOn = false; //이번 프레임에 점프를 하는지 bool moveOn = false; //이번 프레임에 움직이고 있는지 [Tooltip("걷는 소리")] public AudioClip walkSound; [Tooltip("점프 소리")] public AudioClip jumpSound; private AudioSource soundPlay; CharacterController playerCC; //캐릭터 컨트롤러 컴포넌트 Animator ani; //애니메이터 컴포넌트 // Use this for initialization void Awake() { //컴포넌트 수집 playerCC = GetComponent<CharacterController>(); ani = GetComponent<Animator>(); soundPlay = GetComponent<AudioSource>(); //public으로 오브젝트를 끌어당길 수고를 줄이기 위해 고유한 오브젝트들은 Find로 연결한다. //fairy = GameObject.Find("FairyLayer").GetComponent<Fairy>(); model = transform.FindChild("PlayerModel").gameObject; cameraPivot = transform.FindChild("CameraPivot"); respawnPos = GameObject.Find("SpawnThisPos").transform; transform.position = respawnPos.position;//플레이어 캐릭터 위치 초기화 leftButton = GameObject.Find("Left"); rightButton = GameObject.Find("Right"); jumpButton = GameObject.Find("Jump"); actionButton = GameObject.Find("ActionButton"); leftButtonChange = leftButton.GetComponent<ImageChange>(); rightButtonChange = rightButton.GetComponent<ImageChange>(); jumpButtonChange = jumpButton.GetComponent<ImageChange>(); actionButtonChange = actionButton.GetComponent<ImageChange>(); rayMask = (1 << LayerMask.NameToLayer("ButtonLayer")); //터치용 레이 마스트. Button은 이동 관련 버튼들이다. } void Start() //첫 프레임때 보정도 걸어준다. Update에선 보정이 Lerp를 통해 이뤄지기에 보정엔 시간이 걸려서 만약 초기 배치가 잘못되서 캐릭터가 떨어지는 불상사를 막기 위함에 있다. { if (circleMove) //원 보정 { lineMove = false; //언젠가 직진이동으로 바뀌었을 때를 대비해 lineMove를 false로 바꾼다. transform.eulerAngles = new Vector3(0, 90 + GetDegree(new Vector2(radiusCenter.position.x, radiusCenter.position.z), new Vector2(transform.position.x, transform.position.z)), 0); //플레이어 캐릭터의 각도를 중심을 기준으로 옆을 보게 만든다. Vector3 tempt = new Vector3(transform.position.x - radiusCenter.position.x, 0, transform.position.z - radiusCenter.position.z); //중심에서 캐릭터가 있던 각도의 방향을 구한다. Vector3 tempt2 = tempt.normalized * radius; //그리고 그 방향으로 radius 값만큼 보낸다. transform.position = new Vector3(radiusCenter.position.x + tempt2.x, transform.position.y, radiusCenter.position.z + tempt2.z); //구한 좌표를 y축은 놔두고 x와 z축만 적용시킨다. } else { if (!lineMove) //직진이동 첫프레임일시 { lineMove = true; //lineMove를 true로 만들어 재진입 하지 않게 만든다. lineStartPosition = transform.position; //현재 좌표를 기억한다. } Vector3 tempLine = transform.InverseTransformDirection(transform.position - lineStartPosition); //최초 좌표로부터 현재 플레이어 캐릭터의 좌표의 방향을 로컬로 변환시킨다. tempLine.z = 0; //그 로컬 값에서 깊이만 제거하고 transform.position = lineStartPosition + transform.TransformDirection(tempLine); //다시 월드좌표로 변환시켜 적용시킨다. } } // Update is called once per frame void FixedUpdate() //모바일환경에서 30프레임 고정으로 만들기 위해 FixedUpdate 환경에서 이루어진다. { Init(); //프레임 돌때마다 실시간으로 확인해줘야 하는 요소들을 초기화해준다. //if (!die) //원래는 HP가 있어서 HP가 0일때 진입한다. 이 코드에서는 HP를 삭제했기에 사실상 죽지 않는다. //{ // die = true; //한 번만 진입하게 한다. // soundPlay.Stop(); //플레이어 캐릭터가 내는 모든 소리 정지(발소리, 점프소리 등) // nowMoveX = 0; //움직이고 있었다면 정지 // if (0 < nowMoveY) // nowMoveY = 0; //점프 중이었다면 바로 추락 // fairy.StopCoroutine("fixedUpdate"); //슬라이드도 되지 않게 한다. // Invoke("Die", 3.0f); //3초 후에 다시 씬로드 //} if (circleMove) //위에서 한 번 했던 보정들. 여기선 Lerp를 통해 좀 더 자연스러워 보이게 했다. { lineMove = false; transform.eulerAngles = new Vector3(0, 90 + GetDegree(new Vector2(radiusCenter.position.x, radiusCenter.position.z), new Vector2(transform.position.x, transform.position.z)), 0); Vector3 tempt = new Vector3(transform.position.x - radiusCenter.position.x, 0, transform.position.z - radiusCenter.position.z); Vector3 tempt2 = tempt.normalized * radius; transform.position = Vector3.Lerp(transform.position, new Vector3(radiusCenter.position.x + tempt2.x, transform.position.y, radiusCenter.position.z + tempt2.z), Time.deltaTime * 2); } else { if (!lineMove) { lineMove = true; lineStartPosition = transform.position; } Vector3 tempLine = transform.InverseTransformDirection(transform.position - lineStartPosition); tempLine.z = 0; transform.position = Vector3.Lerp(transform.position, lineStartPosition + transform.TransformDirection(tempLine), Time.deltaTime * 2); } if (!die) //죽지 않았을 경우 각종 입력을 받을 수 있다. { InputButton(); } if (!die) { if (leftButtonOn) //해당 버튼들이 눌렸다면 이미지 변경으로 눌렀음을 알림. { leftButtonChange.ChangeTex(true); } else { leftButtonChange.ChangeTex(false); } if (rightButtonOn) { rightButtonChange.ChangeTex(true); } else { rightButtonChange.ChangeTex(false); } if (jumpButtonOn) { jumpButtonChange.ChangeTex(true); } else { jumpButtonChange.ChangeTex(false); } if (actionButtonOn) { actionButtonChange.ChangeTex(true); } else { actionButtonChange.ChangeTex(false); } } else { //죽으면 눌리는 이미지가 강제로 안 뜨게 한다. leftButtonChange.ChangeTex(false); rightButtonChange.ChangeTex(false); jumpButtonChange.ChangeTex(false); actionButtonChange.ChangeTex(false); } Move(); } void OnControllerColliderHit(ControllerColliderHit hit) { //CharactorController 컴포넌트가 Rigidbody에 힘을 행사하기 위해 있는 함수 Rigidbody body; body = hit.collider.attachedRigidbody; //접촉한 콜라이더의 Rigidbody를 가져온다. if (body == null || body.isKinematic) //Rigidbody컴포넌트가 없거나 isKinematic상태라면 리턴한다. return; if (hit.collider.CompareTag("PushBox") && playerCC.isGrounded) //박스를 밀기위한 동작. //조건은 플레이어 캐릭터가 땅을 밟고 있어야 한다. { ani.SetBool("BoxPush", true); //미는 모션을 켜준다. Vector3 tempV;//임시 벡터 int temp = (int)Mathf.Round(model.transform.localEulerAngles.y); //모델링 캐릭터의 로컬 각도는 보고있는 방향을 뜻한다. //Mathf.Round를 해주는 이유는 유니티가 가끔 0.00001의 오차를 내는 경우가 있기 때문 if (temp == 90) //오른쪽으로 박스를 밀고 있다면 { tempV = body.transform.TransformDirection(Vector3.right * 1f); //박스의 형태에 맞춰서 1의 속도로 밀수 있게 벡터를 따온다. body.velocity = new Vector3(tempV.x * 1.5f, body.velocity.y, tempV.z * 1.5f); //박스는 Rigidbody 컴포넌트를 지니고 있고 등속도로 움직일 수 있도록 velocity의 값을 변경시킨다. if (soundPlay.pitch != 1.5f) //점프는 pitch가 1.5배이다. 충돌방지용 soundPlay.pitch = 0.75f; //일반 걸음소리의 0.75배를 주니 소리도 묵직해지고 발 타이밍도 거의 딱 맞는다. } else if (temp == 270 || temp == -90) //왼쪽으로 박스를 밀고 있다면 { //나머지는 위와 반대로 동일 tempV = body.transform.TransformDirection(Vector3.left * 1f); body.velocity = new Vector3(tempV.x * 1.5f, body.velocity.y, tempV.z * 1.5f); if (soundPlay.pitch != 1.5f) soundPlay.pitch = 0.75f; } } } float GetDegree(Vector2 from, Vector2 to) //두 점의 각도 구하는 함수. 원 보정에 쓴다. { return Mathf.Atan2(from.y - to.y, to.x - from.x) * Mathf.Rad2Deg; } void Init() //매 프레임 변수 초기화용. 해당 프레임마다 변화를 체크할 필요가 있는 변수들 { ani.SetBool("BoxPush", false); jumpButtonOn = false; leftButtonOn = false; rightButtonOn = false; actionButtonOn = false; moveOn = false; if (isBoss) //보스전 시 카메라와 방향키 반전. 원래는 보스전 자체가 씬이므로 Awake에서 해주면 되지만 실시간으로 바뀌는걸 보려고 잠시 여기에 두었다. //정상적으로 사용하려면 Awake에 올려서 사용하는 것을 추천 cameraPivot.localEulerAngles = new Vector3(0, 180, 0); else cameraPivot.localEulerAngles = new Vector3(0, 0, 0); } void LeftMove() //왼쪽 이동 시. 왼쪽은 음수 방향. //이후 버튼들의 기본 형태는 모두 동일 { leftButtonOn = true; //화면 UI 표시용 if (!isBoss) //보스전이 아니라면 { if (0 < moveValue) //moveValue가 가려는 방향의 반대 값을 가지고 있다면 일단 0으로 만든다. moveValue = 0; moveValue -= accel * Time.deltaTime; //그리고 가속도만큼 값을 증감한다. if (moveValue <= -1) { moveValue = -1; //-1, 1에 도달하면 넘기지 않도록 한다. } model.transform.localEulerAngles = new Vector3(0, -90, 0); //그리고 모델의 방향을 맞춰준다. } else //보스전은 반대 { if (0 > moveValue) moveValue = 0; moveValue += accel * Time.deltaTime; if (moveValue >= 1) { moveValue = 1; } model.transform.localEulerAngles = new Vector3(0, 90, 0); } moveOn = true; } void RightMove() { rightButtonOn = true; if (!isBoss) { if (moveValue < 0) moveValue = 0; moveValue += accel * Time.deltaTime; if (1 <= moveValue) { moveValue = 1; } model.transform.localEulerAngles = new Vector3(0, 90, 0); } else { if (moveValue > 0) moveValue = 0; moveValue -= accel * Time.deltaTime; if (moveValue <= -1) { moveValue = -1; } model.transform.localEulerAngles = new Vector3(0, -90, 0); } moveOn = true; } void Jump() //점프 { jumpButtonOn = true; if (playerCC.isGrounded) //점프는 땅을 밟고 있어야만 된다. { jumpOn = true; } } void Action() { actionButtonOn = true; //액션버튼. 원래는 레버관련된 코드가 더 있지만 여기선 임시 삭제 } void InputButton() { if (0 < Input.touchCount) //모바일 터치가 있을 경우 { for (int i = 0; i < Input.touchCount; i++) //움직이면서 점프도 할 수 있어야 하고 슬라이드도 해야하므로 멀티터치도 고려한다. { if (Input.GetTouch(i).phase == TouchPhase.Stationary || Input.GetTouch(i).phase == TouchPhase.Moved) //터치 상태가 가만히 있거나 움직일 때 { RaycastHit hit; Ray ray = Camera.main.ScreenPointToRay(Input.GetTouch(i).position); //화면으로 레이를 쏜다. if (Physics.Raycast(ray, out hit, 1000.0f, rayMask)) //마스크를 통해 필요한 부분만 체크 { switch (hit.collider.name) { case "Left": //해당 버튼들을 터치하면 해당 기능을 수행한다. LeftMove(); break; case "Right": RightMove(); break; case "Jump": Jump(); break; case "ActionButton": Action(); break; } } } } } else if (Input.GetMouseButton(0)) //마우스로 클릭했을 경우. 컴퓨터로도 테스트를 하기 위해 마련 { RaycastHit hit; if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hit, 1000.0f, rayMask)) { switch (hit.collider.name) { case "Left": LeftMove(); break; case "Right": RightMove(); break; case "Jump": Jump(); break; case "ActionButton": Action(); break; } } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if (Input.GetAxisRaw("Horizontal") == -1) //키보드 조작. 컴퓨터로 더 용이하게 조작할 수 있도록 마련 { LeftMove(); } else if (Input.GetAxisRaw("Horizontal") == 1) { RightMove(); } if (Input.GetKey(KeyCode.Space)) { Jump(); } if (Input.GetKey(KeyCode.G)) { Action(); } if (!moveOn && moveValue != 0) //버튼을 누르고 있는게 없다면 가속도만큼 가감하여 속도를 0으로 만든다. { if (0 < moveValue) moveValue -= accel * Time.deltaTime; else if (moveValue < 0) moveValue += accel * Time.deltaTime; if (-0.05f < moveValue && moveValue < 0.05f) //0의 근사값에 도달하면 0으로 만들어버린다. moveValue = 0; } } void Move() { if (playerCC.isGrounded) //플레이어 캐릭터가 땅을 밟고 있을 때 { ani.SetBool("isGrounded", true); //애니메이션 파라미터에도 땅을 밟고 있음을 알린다. nowMoveX = moveValue * moveSpeed; //위에서 내려온 moveValue의 값에 입력한 속도를 곱한다. 땅을 밟고 있으면 방향 전환은 바로바로 된다. if (nowMoveX != 0 && !soundPlay.isPlaying) //멈춰있지 않다면 걷는 소리를 낸다. { soundPlay.clip = walkSound; soundPlay.pitch = 1.0f; //상자를 밀때 속도를 낮췄을 수도 있으니 정상으로 돌린다. soundPlay.Play(); } else if (nowMoveX == 0) //움직임이 완전히 정지해 있다면 { soundPlay.Stop(); //소리를 중단시킨다. } nowMoveY = -2.0f; //CharactorController는 아래로 힘이 가해져야 땅을 인식한다. //또한 0에 근접한 값에 있을 때 절벽에서 떨어지면 다소 붕 뜨는 느낌이 있으므로 약간 더 값을 준다. RaycastHit hit; if (Physics.Raycast(model.transform.position, Vector3.down, out hit, 0.3f)) //자신의 발 아래로 레이를 쏴서 땅이 있다면 { nowMoveY = -5.0f; //더 큰 값을 주어 내리막길에서 들썩거리지 않게 한다. //일반적인 절벽에서 떨어지는 상황은 캐릭터의 가장자리에 걸친 상태에서 떨어지므로 레이에 아무것도 닿지 않는다. } if (jumpOn) //점프버튼이 눌렸다면 { nowMoveY = jumpPower; //입력한 힘만큼 점프하고 soundPlay.clip = jumpSound; soundPlay.pitch = 1.5f; //원래 걸음 소리랑 동일한 사운드인데 pitch를 높였더니 더 어울려서 이렇게 쓴다. soundPlay.PlayOneShot(jumpSound);//소리를 출력한다. jumpOn = false; } } else { //공중에 떠있는 상태라면 if (soundPlay.isPlaying && soundPlay.clip == walkSound) { //걷는 소리가 나고 있다면 중단 시킨다. soundPlay.Stop(); } ani.SetBool("isGrounded", false); //애니메이션 파라미터에도 땅을 밟고있지 않음을 알린다. if (moveOn) //움직이는 키 입력 중이라면 { //공중에서 해당 방향의 버튼을 눌렀다면 airControl의 두배의 힘으로 힘이 가해진다. //단, 해당 방향의 속도가 moveSpeed의 값을 넘긴 상황에서 해당 방향의 버튼을 누르면 속도가 늘어나지 않고 유지만 된다. if (0 < moveValue && nowMoveX < moveSpeed) { nowMoveX += airControl * 2; } else if (moveValue < 0 && -moveSpeed < nowMoveX) { nowMoveX -= airControl * 2; } } else { //공중에서 키 입력이 없다면 airControl의 값만큼 천천히 정지하려 한다. if (0 < nowMoveX) { nowMoveX -= airControl; } else if (nowMoveX < 0) { nowMoveX += airControl; } } if (0 < nowMoveY) //점프로 상승 중일때 { if ((playerCC.collisionFlags & CollisionFlags.Above) != 0) //캐릭터 컨트롤러 콜라이더의 윗부분에 뭔가 닿았다면 { nowMoveY = 0; //바로 떨어지게 한다. } } nowMoveY -= gravity * Time.deltaTime; //공중에선 매 프레임 중력가속도만큼 영향을 준다. } moveVector = transform.TransformDirection(new Vector3(nowMoveX, nowMoveY, 0)); //위에서 조합 된 nowMoveX와 nowMoveY는 로컬 기준이므로 월드로 변형시킨다. ani.SetFloat("JumpVelocity", nowMoveY); //애니메이터 파라미터 갱신 ani.SetFloat("Speed", Mathf.Abs(nowMoveX)); //애니메이터 파라미터 갱신. 속도의 경우 절대값으로 받아 음수를 제외한다. if (die == false) { //죽지 않았다면 얻어낸 월드 이동벡터만큼 CharacterController.Move()로 이동 playerCC.Move(moveVector * Time.deltaTime); } else if (die == true) { //죽었다면 움직이진 못하고 떨어질수 있게만 해둔다. //지금은 주석처리를 해놨지만 위에서 죽음을 체크하면 알아서 nowMoveX를 0으로 만들기 때문에 이렇게 구분 안 해줘도 되긴 한다. playerCC.Move(new Vector3(0, moveVector.y, 0) * Time.deltaTime); } } } | cs |
여태까지 코드들 보면 제가 public을 이용해서 다른 스크립트가 영향력을 행사 할 수 있도록 하는 경우가 많은데 사양면에서 저게 제일 이득이라는 소리를 어디선가 들어서 그렇습니다.
아무튼 이 코드를 통해 알 수 있는 것은 크게 다음과 같습니다.
1. 키보드, 마우스, 모바일 터치 동시 인식 가능(모바일 이외는 사실상 테스트용. 매번 빌드하기엔 너무 오래 걸리므로)
2. 해당 기능에 대응하는 버튼 입력시 UI이미지 변경
3. 게임적 허용 수준의 관성 구현
4. Rigidbody 박스를 밀 수 있다.
정도겠네요.
근데 die라는 bool값이 있는데 저건 지금 각종 상호작용 요소를 다 삭제했더니 HP도 필요가 없어져서 사실상 죽을 일이 없어 die가 true가 될 일이 없습니다.
그냥 저런식으로 죽으면 모든게 정지한다 정도만 아시면 될거 같습니다.
최대한 코드를 간결하게 만들고, 삭제하고, 주석달고 했는데 그래도 여전히 깔끔하다는 느낌은 들지 않네요.
이전 글들은 그래도 꽤 만족하게 나왔는데...
역시 코드는 처음부터 잘 짜야하는데.... 더 공부해야겠습니다.
'정리' 카테고리의 다른 글
(유니티3D) FPS 총기 반동 구현하기 1 (1) | 2018.01.24 |
---|---|
(유니티 2D) UGUI Canvas 설정 (0) | 2017.12.28 |
(유니티 2D) 카메라 사이즈와 Pixels Per Unit (1) | 2017.12.28 |
(유니티3D)커스텀 에디터(인스펙터 창 변경) (0) | 2017.12.15 |
(유니티3D)CharactorController용 움직이는 땅 (0) | 2017.12.15 |