본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
https://fastcampus.info/4n8ztzq
Part.4 배틀로얄 제작을 위한 알고리즘과 프레임 워크
CH 04. 플레이어 제작
02. 동작 컨트롤러(Behaviour Controller) 만들기
사전 작업으로 PlayerCharacter를 TestScene에 추가하겠습니다.
예제 프로젝트에 제공된 PolygonBattleRoyale 의 캐릭터 프리팹을 씬에 추가. (참고로 포플용 게임을 제작 중이라면, PolygonBattleRoyale Asset을 직접 사서 진행하기를 강사는 권장) (저는 단순 공부용이라 제공된 프리팹으로 진행했습니다..)
CharacterController 컴포넌트를 추가시킬수도 있지만, 실무에서는 이 컴포넌트가 무겁다고 생각을 해서,
Rigidbody와 Capsule Collider 컴포넌트를 추가시켜 진행함.
현재도 그러한가요?? (chatgpt)
네, 이 부분은 2021년 당시와 지금(2025년)도 기본적인 인식은 크게 변하지 않았습니다.
다만 Unity의 CharacterController 컴포넌트가 계속 업데이트되면서 예전보다 안정성과 성능이 조금 나아졌고, 선택 기준이 조금 더 명확해졌어요.
1. 2021년 당시 인식
- CharacterController
- 장점:
- 이동, 계단 오르기, 슬로프 처리, 충돌 판정이 내장
- 스크립트 작성량이 줄어듦
- 단점:
- Rigidbody 물리엔진과 별도로 동작 → 물리 기반 상호작용이 제한됨
- 내부 구현이 블랙박스라 미세한 제어가 어려움
- 슬라이딩, 경사 처리 커스터마이징이 제한적
- 대규모 다중 캐릭터 환경에서 약간 무거울 수 있음
- 장점:
- Rigidbody + Capsule Collider
- 장점:
- Unity의 물리엔진(PhysX)과 완벽 호환
- 커스터마이징 자유도 높음
- 물리 상호작용(밀기, 튕기기 등) 자연스러움
- 단점:
- 계단/경사 처리, 땅 감지, 충돌 보정 등 직접 구현 필요
- 구현 난이도가 높음
- 장점:
2. 2025년 현재 변화
- Unity 2022~2023 LTS에서 CharacterController 성능 최적화와 Slope Limit, Step Offset 개선이 있었음
- 여전히 물리 기반 동작(총알에 밀림, 폭발 반동, 물리 오브젝트 상호작용)이 많은 게임은 Rigidbody+Collider 선호
- MMO나 TPS/FPS 같은 정교한 물리 반응을 원하는 게임 → 대부분 Rigidbody 선택
- 간단한 싱글플레이, 플랫폼 게임, 3D RPG → CharacterController도 많이 사용
3. 현업 기준 정리
| 선택 기준 | 추천 |
| 정확한 물리 반응 필요 (총, 폭발, 밀기 등) | Rigidbody + CapsuleCollider |
| 빠른 개발 & 간단한 캐릭터 충돌 처리 | CharacterController |
| 네트워크 멀티플레이 FPS/TPS | Rigidbody (예측/보정 구현 용이) |
| 싱글플레이 어드벤처/플랫폼 | CharacterController 가능 |
💡 정리
강사가 2021년에 말한 “CharacterController가 무겁다”는 인식은 지금도 일부 맞지만,
성능 자체보다 물리 상호작용 제약이 선택을 갈라놓는 핵심 이유입니다.
배틀그라운드처럼 물리 기반 FPS/TPS라면 지금도 실무에서는 Rigidbody + Collider가 주류입니다.
그리고 PlayerCharacter 의 Animator 컴포넌트의 Controller 에
프로젝트에 제공된 Character Controller 를 연결시킵니다.

연결된 Character Avatar

언급한 이유는 이를 사용해서 벽에 살짝 기대거나 할 떄, 이런 시스템이 있기 때문에 상체의 본 트랜스폼을 가져와서 작업이 가능하기 때문입니다.
그럼 이제 BehaviourController를 구현해보겠습니다.
(동작들을 관리하는 컨드롤러)

Behaviour들은 특정한 부모 클래스를 갖고, 그 클래스를 상속받은 아이들만 BehaviourController로 제어합니다.
isAttacking, isDead 그런 조건들을 코드로 지저분하게 쓰지 말고,
‘그냥 이거 할 때는 이거 잠겼으니까 이거 돌지마’ 그런거를 깔끔하게 몇 개의 코드로 제어하기 위해서 이렇게 GenericBehaviour를 따로 만들어서 진행합니다.

Behaviour Contorller 에 필요한 변수들을 추가했습니다. (주석 참고)
📜 BehaviourController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 현재 동작, 기본 동작, 오버라이딩 동작, 잠긴 동작, 마우스 이동값,
/// 땅에 서있는지, GenericBehaviour를 상속받은 동작들을 업데이트 시켜줍니다.
/// </summary>
public class BehaviourController : MonoBehaviour
{
private List<GenericBehaviour> behaviours; // 동작들
private List<GenericBehaviour> overrideBehaviours; // 우선시 되는 동작
private int currentBehaviour; // 현재 동작 해시코드
private int defaultBehaviour; // 기본 동작 해시코드
private int behaviourLocked; // 잠긴 동작 해시코드
// 캐싱
public Transform playerCamera;
private Animator myAnimator;
private Rigidbody myRigidbody;
private ThirdPersonOrbitCam camScript;
private Transform myTransform;
//
private float h; // horizontal axis
private float v; // vertical axis
public float turnSmoothing = 0.06f; // 카메라를 향하도록 움직일 때, 회전속도
private bool changedFOV; // 달리기 동작이 카메라 시야각이 변경되었을 때 저장되었나
public float sprintFOV = 100; // 달리기 시야각
private Vector3 lastDirection; // 마지막 향했던 방향
private bool sprint; // 달리는 중인가?
private int hFloat; // 애니메이터 관련 가로축 값.
private int vFloat; // 애니메이터 관련 세로축 값.
private int groundedBool; // 애니메이터 지상에 있는가.
private Vector3 colExtents; // 땅과의 충돌체크를 위한 충돌체 영역
public float GetH { get => h; }
public float GetV { get => v; }
public ThirdPersonOrbitCam GetCamScript { get => camScript; }
public Rigidbody GetRigidbody { get => myRigidbody; }
public Animator GetAnimator { get => myAnimator; }
public int GetDefaultBehaviour { get => defaultBehaviour; }
private void Awake()
{
behaviours = new List<GenericBehaviour>();
overrideBehaviours = new List<GenericBehaviour>();
myAnimator = GetComponent<Animator>();
hFloat = Animator.StringToHash(FC.AnimatorKey.Horizontal);
vFloat = Animator.StringToHash(FC.AnimatorKey.Vertical);
camScript = playerCamera.GetComponent<ThirdPersonOrbitCam>();
myRigidbody = GetComponent<Rigidbody>();
myTransform = transform;
//ground?
groundedBool = Animator.StringToHash(FC.AnimatorKey.Grounded);
colExtents = GetComponent<Collider>().bounds.extents;
}
public bool IsMoving()
{
// return (h != 0) || (v != 0); <- 위험한 코드!! WHy?? 부동 소수점!! 때문에 문제가 발생할 수 있죠!!
return Mathf.Abs(h) > Mathf.Epsilon || Mathf.Abs(v) > Mathf.Epsilon;
}
public bool IsHorizontalMoving()
{
return Mathf.Abs(h) > Mathf.Epsilon;
}
public bool CanSprint()
{
foreach(GenericBehaviour behaviour in behaviours)
{
if(!behaviour.AllowSprint)
{
return false;
}
}
foreach(GenericBehaviour behaviour in overrideBehaviours)
{
if(!behaviour.AllowSprint)
{
return false;
}
}
return true;
}
public bool IsSprinting()
{
return sprint && IsMoving() && CanSprint();
}
public bool IsGrounded()
{
Ray ray = new Ray(myTransform.position + Vector3.up * 2 * colExtents.x, Vector3.down);
return Physics.SphereCast(ray, colExtents.x, colExtents.x + 0.2f);
}
private void Update()
{
h = Input.GetAxis("Horizontal");
v = Input.GetAxis("Vertical");
myAnimator.SetFloat(hFloat, h, 0.1f, Time.deltaTime);
myAnimator.SetFloat(vFloat, v, 0.1f, Time.deltaTime);
sprint = Input.GetButton(ButtonName.Sprint);
if(IsSprinting())
{
changedFOV = true;
camScript.SetFOV(sprintFOV);
}
else if(changedFOV)
{
camScript.ResetFOV();
changedFOV = false;
}
myAnimator.SetBool(groundedBool, IsGrounded());
}
public void Repositioning()
{
if(lastDirection != Vector3.zero)
{
lastDirection.y = 0f;
Quaternion targetRotation = Quaternion.LookRotation(lastDirection);
Quaternion newRotation = Quaternion.Slerp(myRigidbody.rotation, targetRotation, turnSmoothing);
myRigidbody.MoveRotation(newRotation);
}
}
private void FixedUpdate()
{
bool isAnyBehaviourActive = false;
if(behaviourLocked > 0 || overrideBehaviours.Count == 0)
{
foreach(GenericBehaviour behaviour in behaviours)
{
if(behaviour.isActiveAndEnabled && currentBehaviour == behaviour.GetBehaviourCode)
{
isAnyBehaviourActive = true;
behaviour.LocalFixedUpdate();
}
}
}
else
{
foreach(GenericBehaviour behaviour in overrideBehaviours)
{
behaviour.LocalFixedUpdate();
}
}
if(!isAnyBehaviourActive && overrideBehaviours.Count == 0)
{
myRigidbody.useGravity = true;
Repositioning();
}
}
private void LateUpdate()
{
if(behaviourLocked > 0 || overrideBehaviours.Count == 0)
{
foreach(GenericBehaviour behaviour in behaviours)
{
if(behaviour.isActiveAndEnabled && currentBehaviour == behaviour.GetBehaviourCode)
{
behaviour.LocalLateUpdate();
}
}
}
else
{
foreach(GenericBehaviour behaviour in overrideBehaviours)
{
behaviour.LocalLateUpdate();
}
}
}
public void SubScribeBehaviour(GenericBehaviour behaviour)
{
behaviours.Add(behaviour);
}
public void RegisterDefaultBehaviour(int behaviourCode)
{
defaultBehaviour = behaviourCode;
currentBehaviour = behaviourCode;
}
public void RegisterBehaviour(int behaviourCode)
{
if(currentBehaviour == defaultBehaviour)
{
currentBehaviour = behaviourCode;
}
}
public void UnRegisterBehaviour(int behaviourCode)
{
if(currentBehaviour == behaviourCode)
{
currentBehaviour = defaultBehaviour;
}
}
public bool OverrideWithBehaviour(GenericBehaviour behaviour)
{
if (!overrideBehaviours.Contains(behaviour))
{
if(overrideBehaviours.Count == 0)
{
foreach(GenericBehaviour behaviour1 in behaviours)
{
if(behaviour1.isActiveAndEnabled && currentBehaviour == behaviour1.GetBehaviourCode)
{
behaviour1.OnOverride();
break;
}
}
}
overrideBehaviours.Add(behaviour);
return true;
}
return false;
}
public bool RevokeOverridingBehaviour(GenericBehaviour behaviour)
{
if(overrideBehaviours.Contains(behaviour))
{
overrideBehaviours.Remove(behaviour);
return true;
}
return false;
}
public bool IsOverriding(GenericBehaviour behaviour = null)
{
if(behaviour == null)
{
return overrideBehaviours.Count > 0;
}
return overrideBehaviours.Contains(behaviour);
}
public bool IsCurrentBehaviour(int behaviourCode)
{
return this.currentBehaviour == behaviourCode;
}
public bool GetTempLockStatus(int behaviourCode = 0)
{
return (behaviourLocked != 0 && behaviourLocked != behaviourCode);
}
public void LockTempBehaviour(int behaviourCode)
{
if(behaviourLocked == 0)
{
behaviourLocked = behaviourCode;
}
}
public void UnLcokTempBehaviour(int behaviourCode)
{
if(behaviourLocked == behaviourCode)
{
behaviourLocked = 0;
}
}
public Vector3 GetLastDirection()
{
return lastDirection;
}
public void SetLastDirection(Vector3 direction)
{
lastDirection = direction;
}
}
public abstract class GenericBehaviour : MonoBehaviour
{
protected int speedFloat;
protected BehaviourController behaviourController;
protected int behaviourCode;
protected bool canSprint;
private void Awake()
{
this.behaviourController = GetComponent<BehaviourController>();
speedFloat = Animator.StringToHash(FC.AnimatorKey.Speed);
canSprint = true;
// 동작 타입을 해시코드로 가지고있다가 추후에 구별용으로 사용.
behaviourCode = this.GetType().GetHashCode();
}
public int GetBehaviourCode
{
get => behaviourCode;
}
public bool AllowSprint
{
get => canSprint;
}
public virtual void LocalLateUpdate()
{
}
public virtual void LocalFixedUpdate()
{
}
public virtual void OnOverride()
{
}
}
이 BehaviourController 클래스는
플레이어 캐릭터의 **"플러거블 동작 시스템"**을 관리하는 중앙 컨트롤러 역할을 합니다.
즉, 걷기, 달리기, 점프, 조준, 발사 등 **여러 동작(Behaviour)**을 독립 모듈로 만들고,
그 중 어떤 동작을 현재 활성화할지, 우선시할지, 잠글지 등을 제어합니다.
📌 핵심 개념
- GenericBehaviour 기반 모듈 관리
- 모든 동작은 GenericBehaviour를 상속받아 구현
- 동작 고유 ID(behaviourCode)로 구분
- 기본 동작 / 현재 동작 / 오버라이딩 동작 / 잠긴 동작 상태 관리
- 입력값 및 상태 추적
- h, v: 이동 입력 (Horizontal / Vertical Axis)
- sprint: 달리기 여부
- IsGrounded()로 지면 감지
- Animator 파라미터(hFloat, vFloat, groundedBool) 갱신
- 카메라 FOV 변경(달리기 시 확대)
- 동작 업데이트 흐름
- Update()
- 입력값 처리, 애니메이션 파라미터 갱신
- 달리기 가능 여부 체크 및 카메라 시야 변경
- FixedUpdate()
- 현재 동작 또는 오버라이딩 동작의 LocalFixedUpdate() 호출
- 아무 동작도 없으면 중력 적용 및 마지막 방향 회전 보정
- LateUpdate()
- LateUpdate 단계에서 동작 모듈의 LocalLateUpdate() 호출
- Update()
- 동작 관리 기능
- SubScribeBehaviour() : 동작 등록
- RegisterDefaultBehaviour() : 기본 동작 지정
- OverrideWithBehaviour() : 특정 동작을 임시로 우선 적용
- RevokeOverridingBehaviour() : 오버라이드 취소
- LockTempBehaviour() / UnLockTempBehaviour() : 특정 동작 잠금 / 해제
- IsOverriding(), IsCurrentBehaviour() 등 상태 조회 메서드 제공
📜 한 줄 요약
BehaviourController는
플레이어의 여러 독립 동작 모듈을 플러그처럼 연결·해제하면서, 입력·애니메이션·카메라를 통합 관리하는 중앙 허브입니다.
학습 인증 사진
① 오늘자 날짜, 공부 시작 시각 포함 사진 1장
② 오늘자 날짜, 공부 종료 시각 포함 사진 1장


③ 1개 클립 수강 인증 사진 (강의장 목록 캡쳐, 강의 내용이 담긴 수강화면이 보이지 않도록) 1장

④ 학습 인증샷 1장 이상 (ex. 필기 촬영, 작업물, 등)


⑤ 마이페이지 - 학습통계 - 챌린지 대상 강의 완강률 캡쳐 사진 1장 (완료 클립 수가 보이도록)
