목록Unity Engine (27)
[멋쟁이사자처럼부트캠프 유니티 게임 개발 7기] 2026년 2월 24일 회고록 - State Machine 2, Delegate, 2D Light, 쉐이더 그래프
2026.02.24김산나
2026_03_06 강의 요약본
1. Vroid (+Blend Tree)
2. 뒤끝 베이직 세팅
3. 포톤 세팅
1. Vroid
3D 캐릭터를 쉽게 뽑을 수 있는 프로그램.
완전 무료이고, 직접 꾸민 캐릭터는 만든 사람에게 저작권이 귀속된다.
다운로드
체형, 얼굴, 헤어 등 전부 커스텀이 가능하고, 기본적으로 텍스쳐를 수정하는 방식으로 꾸미기 때문에 허접해 보일 수 있다.
다만 블렌더로 모델을 가져와 추가적으로 수정할 수 있기 때문에 충분히 보완할 수 있다.

vrm으로 파일을 export하면 된다.
이 파일은 유니티에서 정상적으로 읽히려면
https://github.com/vrm-c/UniVRM/releases/tag/v0.131.0
Release v0.131.0 · vrm-c/UniVRM
Installation You can install UniVRM using the UnityPackage or the UPM Package. The UniVRM supports Unity 2022.3 LTS or later. UnityPackage Download the unitypackage, and drag and drop it to import ...
github.com
해당 패키지를 다운받으면 된다.
자체적인 툰 셰이더가 있는 것 같다.

이 캐릭터에 애니메이션을 주고 싶다.
이동의 경우에는 "속도"값을 기준으로 애니메이션을 적용하면 자연스러울 것 같다.
그때 사용되는 기능이 "Blend Tree"이다.


Speed 파라미터 생성. (float)
애니메이터 > 블렌드 트리 생성
더블클릭하면

이런 게 뜬다.
이걸 선택하면

이런 창이 뜨는데, Motion List의 +버튼을 눌러 조정할 수 있다.
속도값이 얼마일 때까지 저 모션을 출력하는지 설정한다.
2. 뒤끝 베이스 세팅
https://docs.backnd.com/sdk-docs/backend/base/start-up/
시작하기 | 뒤끝 개발자
SDK 연동 과정을 영상으로 확인하세요!
docs.backnd.com
해당 문서를 읽으면 된다.
SDK를 다운받고, 프로젝트를 생성하여 Client ID, Signature Key를 세팅값에 넣어주면 된다.
<코드>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 뒤끝 SDK namespace 추가
using BackEnd;
public class BackendLogin
{
private static BackendLogin _instance = null;
public static BackendLogin Instance
{
get
{
if (_instance == null)
{
_instance = new BackendLogin();
}
return _instance;
}
}
public void CustomSignUp(string id, string pw)
{
Debug.Log("회원가입을 요청합니다.");
var bro = Backend.BMember.CustomSignUp(id, pw);
if (bro.IsSuccess())
{
Debug.Log("회원가입에 성공했습니다. : " + bro);
}
else
{
Debug.LogError("회원가입에 실패했습니다. : " + bro);
}
}
public void CustomLogin(string id, string pw)
{
Debug.Log("로그인을 요청합니다.");
var bro = Backend.BMember.CustomLogin(id, pw);
if (bro.IsSuccess())
{
Debug.Log("로그인이 성공했습니다. : " + bro);
}
else
{
Debug.LogError("로그인이 실패했습니다. : " + bro);
}
}
public void UpdateNickname(string nickname)
{
Debug.Log("닉네임 변경을 요청합니다.");
var bro = Backend.BMember.UpdateNickname(nickname);
if (bro.IsSuccess())
{
Debug.Log("닉네임 변경에 성공했습니다 : " + bro);
}
else
{
Debug.LogError("닉네임 변경에 실패했습니다 : " + bro);
}
}
}
회원가입, 로그인, 닉변 세 가지 기능을 구현하는 코드.
나중에는 인폿값을 받아 직접 구현하면 된다.
using UnityEngine;
// 뒤끝 SDK namespace 추가
using BackEnd;
public class BackendManager : MonoBehaviour
{
void Start()
{
var bro = Backend.Initialize(); // 뒤끝 초기화
// 뒤끝 초기화에 대한 응답값
if (bro.IsSuccess())
{
Debug.Log("초기화 성공 : " + bro); // 성공일 경우 statusCode 204 Success
}
else
{
Debug.LogError("초기화 실패 : " + bro); // 실패일 경우 statusCode 400대 에러 발생
}
Test();
}
void Test()
{
BackendLogin.Instance.CustomLogin("user1", "1234"); // [추가] 뒤끝 회원가입 함수
BackendLogin.Instance.UpdateNickname("김산나"); // [추가] 뒤끝 닉네임 변경 함수
Debug.Log("테스트를 종료합니다.");
}
}
실제 서버와 연결하는 코드.
3. 포톤 세팅
포톤은 20명 동접까지 무료이다.
기본적으로 클라이언트끼리 연동하는 방식을 갖고 있다.
PUN 2 - FREE | 네트워크 | Unity Asset Store
Get the PUN 2 - FREE package from Photon Engine and speed up your game development process. Find this & other 네트워크 options on the Unity Asset Store.
assetstore.unity.com
먼저 포톤 전용 패키지를 다운받는다.
https://www.photonengine.com/ko-kr
그 다음 포톤에 가입하여 프로젝트를 생성한다. 포톤 종류는 PUN, 멀티게임 선택


포톤 위자드 열기 (이미 열려있으면 거기서 진행)

Setup Project 선택

프로젝트의 ID부분을 클릭하면 복붙할 수 있게 되는데, 복사해서 AppID에 넣어주면 된다.
<서버 코드 (방 생성 및 플레이어 배치)>
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;
public class PhotonManger : MonoBehaviourPunCallbacks
{
[Header("Room Settings")]
[SerializeField] private string roomName = "KatanaRoom";
[SerializeField] private byte maxPlayers = 4;
[Header("Player")]
[SerializeField] private string playerPrefabName = "Player"; // Resources 폴더 내 프리팹 이름
[SerializeField] private Transform spawnPoint; // 없으면 원점 스폰
void Start()
{
PhotonNetwork.ConnectUsingSettings();
}
// 마스터 서버 연결 완료 → 로비 입장
public override void OnConnectedToMaster()
{
Debug.Log("마스터 서버 연결됨. 로비 입장 중...");
PhotonNetwork.JoinLobby();
}
// 로비 입장 완료 → 방 참가 또는 생성
public override void OnJoinedLobby()
{
Debug.Log("로비 입장 완료. 방 참가 시도...");
RoomOptions options = new()
{
MaxPlayers = maxPlayers,
IsOpen = true,
IsVisible = true
};
PhotonNetwork.JoinOrCreateRoom(roomName, options, TypedLobby.Default);
}
// 방 입장 완료 → 캐릭터 스폰
public override void OnJoinedRoom()
{
Debug.Log($"방 입장 완료. 플레이어 수: {PhotonNetwork.CurrentRoom.PlayerCount}");
SpawnPlayer();
}
// 방 생성 실패 시 재시도
public override void OnCreateRoomFailed(short returnCode, string message)
{
Debug.LogWarning($"방 생성 실패: {message}. 재참가 시도...");
PhotonNetwork.JoinOrCreateRoom(roomName, new RoomOptions { MaxPlayers = maxPlayers }, TypedLobby.Default);
}
// 방 참가 실패 시 새 방 생성
public override void OnJoinRoomFailed(short returnCode, string message)
{
Debug.LogWarning($"방 참가 실패: {message}. 새 방 생성 중...");
PhotonNetwork.CreateRoom(roomName, new RoomOptions { MaxPlayers = maxPlayers });
}
// 다른 플레이어 입장 로그
public override void OnPlayerEnteredRoom(Photon.Realtime.Player newPlayer)
{
Debug.Log($"{newPlayer.NickName} 입장. 현재 인원: {PhotonNetwork.CurrentRoom.PlayerCount}");
}
// 다른 플레이어 퇴장 로그
public override void OnPlayerLeftRoom(Photon.Realtime.Player otherPlayer)
{
Debug.Log($"{otherPlayer.NickName} 퇴장. 현재 인원: {PhotonNetwork.CurrentRoom.PlayerCount}");
}
// 연결 끊김 처리
public override void OnDisconnected(DisconnectCause cause)
{
Debug.LogWarning($"연결 끊김: {cause}. 재연결 시도...");
PhotonNetwork.ConnectUsingSettings();
}
private void SpawnPlayer()
{
Vector3 pos = spawnPoint != null ? spawnPoint.position : Vector3.zero;
Quaternion rot = spawnPoint != null ? spawnPoint.rotation : Quaternion.identity;
PhotonNetwork.Instantiate(playerPrefabName, pos, rot);
}
}
<플레이어 움직임>
using Photon.Pun;
using UnityEngine;
public class Player : MonoBehaviourPun, IPunObservable
{
[Header("Movement")]
[SerializeField] private float walkSpeed = 3f;
[SerializeField] private float runSpeed = 6f;
[SerializeField] private float rotationSmoothTime = 0.1f;
[Header("Animation")]
[SerializeField] private float animationDampTime = 0.1f;
[Header("Network Sync")]
[SerializeField] private float positionLerpSpeed = 10f;
[SerializeField] private float rotationLerpSpeed = 10f;
private Animator animator;
private Rigidbody rb;
private Transform cameraTransform;
private float vertical;
private float horizontal;
private bool isRunning;
private float rotationVelocity;
// 원격 플레이어 동기화용
private Vector3 networkPosition;
private Quaternion networkRotation;
private float networkAnimSpeed;
private static readonly int SpeedHash = Animator.StringToHash("Speed");
void Awake()
{
animator = GetComponent<Animator>();
rb = GetComponent<Rigidbody>();
rb.freezeRotation = true;
rb.interpolation = RigidbodyInterpolation.Interpolate;
networkPosition = transform.position;
networkRotation = transform.rotation;
// 내 캐릭터만 카메라 사용, 원격 플레이어는 물리 비활성화
if (photonView.IsMine)
{
cameraTransform = Camera.main.transform;
}
else
{
rb.isKinematic = true;
}
}
void Update()
{
if (photonView.IsMine)
{
HandleInput();
}
else
{
// 원격 플레이어: 수신한 위치/회전으로 보간
transform.SetPositionAndRotation(
Vector3.Lerp(transform.position, networkPosition, Time.deltaTime * positionLerpSpeed),
Quaternion.Lerp(transform.rotation, networkRotation, Time.deltaTime * rotationLerpSpeed)
);
animator.SetFloat(SpeedHash, networkAnimSpeed, animationDampTime, Time.deltaTime);
}
}
void FixedUpdate()
{
if (!photonView.IsMine) return;
Vector2 inputDir = new(horizontal, vertical);
if (inputDir.sqrMagnitude < 0.01f)
{
rb.linearVelocity = new Vector3(0f, rb.linearVelocity.y, 0f);
return;
}
// 카메라 기준 이동 방향 계산
Vector3 camForward = Vector3.ProjectOnPlane(cameraTransform.forward, Vector3.up).normalized;
Vector3 camRight = Vector3.ProjectOnPlane(cameraTransform.right, Vector3.up).normalized;
Vector3 moveDir = (camForward * vertical + camRight * horizontal).normalized;
// 이동 방향으로 캐릭터 부드럽게 회전
float targetAngle = Mathf.Atan2(moveDir.x, moveDir.z) * Mathf.Rad2Deg;
float smoothAngle = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngle, ref rotationVelocity, rotationSmoothTime);
rb.MoveRotation(Quaternion.Euler(0f, smoothAngle, 0f));
// 이동
float currentSpeed = isRunning ? runSpeed : walkSpeed;
Vector3 velocity = moveDir * currentSpeed;
rb.linearVelocity = new Vector3(velocity.x, rb.linearVelocity.y, velocity.z);
}
// Photon 직렬화: 위치/회전/애니메이션 동기화
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.IsWriting)
{
// 내 데이터를 전송
stream.SendNext(transform.position);
stream.SendNext(transform.rotation);
stream.SendNext(animator.GetFloat(SpeedHash));
}
else
{
// 원격 데이터 수신
networkPosition = (Vector3)stream.ReceiveNext();
networkRotation = (Quaternion)stream.ReceiveNext();
networkAnimSpeed = (float)stream.ReceiveNext();
}
}
private void HandleInput()
{
vertical = Input.GetAxisRaw("Vertical");
horizontal = Input.GetAxisRaw("Horizontal");
isRunning = Input.GetKey(KeyCode.LeftShift);
float animSpeed = 0f;
if (vertical != 0f || horizontal != 0f)
animSpeed = isRunning ? runSpeed : walkSpeed;
animator.SetFloat(SpeedHash, animSpeed, animationDampTime, Time.deltaTime);
}
}
<카메라 세팅용 코드>
using Photon.Pun;
using Unity.Cinemachine;
using UnityEngine;
public class CameraConnector : MonoBehaviour
{
[Tooltip("플레이어 프리팹 안의 CameraTarget 자식 이름")]
[SerializeField] private string cameraTargetName = "CameraTarget";
private CinemachineCamera cinemachineCamera;
void Awake()
{
cinemachineCamera = GetComponent<CinemachineCamera>();
}
void Update()
{
if (cinemachineCamera.Follow != null) return;
// 로컬 플레이어의 CameraTarget을 탐색 (연결되면 더 이상 실행 안 함)
GameObject[] players = GameObject.FindGameObjectsWithTag("Player");
foreach (GameObject player in players)
{
PhotonView pv = player.GetComponent<PhotonView>();
if (pv == null || !pv.IsMine) continue;
Transform target = player.transform.Find(cameraTargetName);
if (target == null)
{
Debug.LogWarning($"[CameraConnector] '{cameraTargetName}' 자식을 찾을 수 없습니다.");
return;
}
cinemachineCamera.Follow = target;
cinemachineCamera.LookAt = target;
Debug.Log("[CameraConnector] 카메라 타겟 연결 완료.");
return;
}
}
}

플레이어 오브젝트 (vrm파일 오브젝트)에 플레이어 컨트롤러, 포톤 뷰, 트렌스폼 뷰, 애니메이터 뷰를 세팅한다.
참고로 애니메이터 뷰는 Disabled로 되어있을 시 작동을 하지 않는다.
카메라에 카메라 커넥터 코드를 연결하고, 플레이를 하면 정상 작동한다.
===========================================================
멀티께임을 만들 수 있게 됐다 !

'Unity Engine' 카테고리의 다른 글
2026_02_25 강의 요약본
- 3D 입문 -
강사님께서는 그냥 z축 하나 추가된 거다 말씀하셨지만, 실제로는 그것보다 훨씬 복잡했다.
왜냐하면 3D의 지향점은 2D보다 " 실제 "에 가까워 사실적인 그래픽을 위한 기술들이 매우매우 많이 들어가 있었다.
일단 요구 사양부터 확 올라간다. 간단히 풀을 심었을 뿐이었는데도 크래쉬 오류가 뜨고 그랬다.
1. 복습
이전에 잠깐 진행했던 3D에서 사용한 코드에 대한 복습이다.
이동, 회전, 타겟 추적, 타겟 방향으로 회전, 충돌 처리, 오브젝트 클릭 순서로 다시 알아보자.
1) 이동
void Update()
{
float speed = 5f;
float h = Input.GetAxis("Horizontal"); // A/D 또는 ←/→
float v = Input.GetAxis("Vertical"); // W/S 또는 ↑/↓
// Space.Self (기본값): 오브젝트의 로컬 방향으로 이동 (앞이 forward)
transform.Translate(Vector3.forward * v * speed * Time.deltaTime, Space.Self);
transform.Translate(Vector3.right * h * speed * Time.deltaTime, Space.Self);
// Space.World: 월드 좌표 기준으로 이동
transform.Translate(Vector3.forward * v * speed * Time.deltaTime, Space.World);
transform.Translate(Vector3.right * h * speed * Time.deltaTime, Space.World);
}
가장 기본적인 이동. Self는 로컬 방향으로 이동, World는 월드 좌표 기준으로 이동한다.
Vector 2는 up, down right, left이런 식으로 있었다면(2차원), Vector3부터는 front가 추가되었다.
2) 회전
void Update()
{
gameObject.transform.eulerAngles += new Vector3(0, 10,0);
}
position도 그렇듯, 각도도 직접 수정이 안 된다. rotation이 아닌, 오일러 각을 수정해야 하고, 각을 Vector3로 입력한다.
3) 타겟 추적
[SerializeField] private Transform target;
[SerializeField] private float speed = 3f;
void Update()
{
// MoveTowards: 일정 속도로 목표 지점 접근 (오버슛 없음)
transform.position = Vector3.MoveTowards(
transform.position,
target.position,
speed * Time.deltaTime
);
// Lerp (선형 보간): 부드럽게 점점 느려지며 접근 (t는 0~1)
// 주의: t에 Time.deltaTime을 사용하면 매 프레임 비율이 달라지므로 아래는 근사치 사용법
transform.position = Vector3.Lerp(
transform.position,
target.position,
0.1f // 매 프레임 나머지 거리의 10% 이동 → 점점 느려짐
);
float rotateSpeed = 90f; // 초당 90도
// Y축 회전
transform.Rotate(0, rotateSpeed * Time.deltaTime, 0, Space.World);
// 로컬 X축 회전 (피칭)
transform.Rotate(Vector3.right * rotateSpeed * Time.deltaTime, Space.Self);
}
두 가지 방식이 있다. MoveTowards, trandform.position = Vector3.Lerp
전자는 이름 그대로 그쪽으로 가는 기능이고, Lerp는 목표 위치로 원하는 곡선대로 이동시키는 기능이다.
4) 타겟 방향으로 회전
[SerializeField] private Transform target;
void Update()
{
// 즉시 target을 바라봄
transform.LookAt(target);
// Y축만 회전하도록 (수직 회전 무시)
Vector3 lookPos = target.position;
lookPos.y = transform.position.y;
transform.LookAt(lookPos);
}
LookAt은 target 위치로 각도를 변경한다.
만약 특정 축만 이동하고 싶다면 Vector3 변수를 만들어 특정 축의 값만 받아서 넣으면 된다.
5) 충돌 처리
2D랑 동일하다. 사실 큰 차이 없다고 봐도 될듯.
다만 이름 뒤에 "2D"를 제외해야 한다.
// 충돌이 시작될 때 (한 번만)
void OnCollisionEnter(Collision collision)
{
Debug.Log("충돌 시작: " + collision.gameObject.name);
// 충돌 지점 정보
foreach (ContactPoint contact in collision.contacts)
{
Debug.DrawRay(contact.point, contact.normal, Color.red, 1f);
}
}
// 충돌 중 (매 프레임)
void OnCollisionStay(Collision collision)
{
// 지속적인 충돌 처리
}
// 충돌이 끝날 때 (한 번만)
void OnCollisionExit(Collision collision)
{
Debug.Log("충돌 종료: " + collision.gameObject.name);
}
// 트리거 영역에 진입할 때
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
Debug.Log("플레이어가 트리거 영역에 진입!");
// 아이템 획득, 체크포인트 등
}
}
// 트리거 영역에서 나갈 때
void OnTriggerExit(Collider other)
{
Debug.Log(other.name + "이 트리거 영역을 벗어남");
}
비슷하다.
6) 오브젝트 클릭
카메라를 기준으로, 유저가 클릭한 위치에 레이저를 쏴 감지된 오브젝트를 클릭 대상으로 지정한다.
void Update()
{
// 마우스 클릭 위치의 오브젝트 감지
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100f))
{
Debug.Log("클릭한 오브젝트: " + hit.collider.gameObject.name);
Debug.Log("충돌 지점: " + hit.point);
Debug.DrawRay(ray.origin, ray.direction * hit.distance, Color.green, 1f);
hit.collider.GetComponent<MeshRenderer>().material.color = Color.red;
}
}
}
2. Terrain
3D의 Plane이랑 비슷하게 생겼는데, 지형을 전문적으로 만드는 Plane이라고 보면 된다.

생성하면 도구가 여럿 있다.

Paint Terrain툴을 누른 후 선택지를 살펴보자.
Rate or Lower Terrain은 산이나 협곡을 그리는 툴이다.

이게 다 브러쉬. 하단에 있는 브러쉬 사이즈, 블투명도를 조절해서 그릴 수 있다.
클릭은 산 그리기, shift + 클릭은 모양대로 고도 낮추기(협곡 그리기)이다.
Paint Texture은 Terrain의 텍스쳐를 그리는 툴이다.

레이어를 분리하여 그릴 수 있다.

이건 잔디, 풀, 나무 등의 요소를 그려주는 툴이다. 하단의 Edit Details로 구조물을 추가, 편집할 수 있다.
최적화 안 되는 툴을 밀도 높게 그리면 게임이 터지니 주의한다....
물은 어떻게 만들까?
물은 Plane에 Material을 적용해서 만든다.

이런 식으로 사이즈를 조절하고, 머터리얼을 적용하면 잘 작동한다.
===========================================================
3D 입문 !
터레인을 사용할 수 있게 되었다 !

'Unity Engine' 카테고리의 다른 글
| [멋쟁이사자처럼부트캠프 유니티 게임 개발 7기] 2026년 5월 26일 회고록 - Unity Netcode (0) | 2026.05.26 |
|---|---|
| [멋쟁이사자처럼부트캠프 유니티 게임 개발 7기] 2026년 3월 6일 회고록 - 멀티 께임 구현 (0) | 2026.03.06 |
| [멋쟁이사자처럼부트캠프 유니티 게임 개발 7기] 2026년 2월 24일 회고록 - State Machine 2, Delegate, 2D Light, 쉐이더 그래프 (0) | 2026.02.24 |
| [멋쟁이사자처럼부트캠프 유니티 게임 개발 7기] 2026년 2월 23일 회고록 - State Machine(FSM), 구글 플레이 스토어 출시 및 애드몹 (0) | 2026.02.23 |
| 유니티 세팅 (0) | 2026.02.11 |
[멋쟁이사자처럼부트캠프 유니티 게임 개발 7기] 2026년 2월 24일 회고록 - State Machine 2, Delegate, 2D Light, 쉐이더 그래프
김산나 2026. 2. 24. 17:332026_02_24 강의 요약본
1. State Machine
어제 알아본 State Machine은 FSM이라는 방식이다.
로직이 단순하다면 swich로 작성해도 괜찮지만, 만약 상태 종류가 늘어나 복잡도가 올라가면 가독성이 확 떨어질 수 있다.
이럴 때는 switch의 case가 아닌, 클래스 단위로 제어하는 편이 좋다.
정확히는 인터페이스를 활용하는 방식이다.
using UnityEngine;
public interface IEnemyState
{
// 상태 이름
string Name {get;}
// 상태에 진입할 때 한 번 호출
void Enter(EnemyController enemy);
// 상태에 있는 동안 매 프레임 호출
void Update(EnemyController enemy);
// 상태를 벗어날 때 한 번 호출
void Exit(EnemyController enemy);
}
이런 식으로 인터페이스를 선언한다. 상태명, Enter, Update, Exit으로 틀을 만든다.
그리고 상태 제어를 위한 컨트롤러를 제작한다.
이 컨트롤러는 Enemy 오브젝트에 부착할 예정이다.
using System.Collections.Generic;
using UnityEngine;
// EnemyController.cs — State Pattern의 Context
//
// ■ Session 8 EnemyFSM과의 핵심 차이
// EnemyFSM: switch (currentState) { case Idle: ... case Chase: ... }
// EnemyController: currentState.Update(this); ← 이게 전부!
//
// ■ 자료구조 활용
// - Dictionary : 상태 이름 → Color 매핑 (세션6 복습)
// - Stack : 상태 전환 히스토리 (세션2 복습)
// - Interface : IEnemyState 다형성 (List<IEnemyState> 저장 가능)
public class EnemyController : MonoBehaviour
{
// ── 상태 인스턴스 ──────────────────────────────────────────
// 미리 생성해두고 재사용 (new 남발 방지)
[HideInInspector] public IdleState idleState = new IdleState();
[HideInInspector] public ChaseState chaseState = new ChaseState();
[HideInInspector] public AttackState attackState = new AttackState();
[HideInInspector] public FleeState fleeState = new FleeState();
IEnemyState currentState;
// ── Inspector 설정값 ───────────────────────────────────────
[Header("플레이어 참조")]
public Transform player;
[Header("AI 설정")]
public float chaseRange = 5f;
public float attackRange = 1.5f;
public float fleeDistance = 7f;
public float moveSpeed = 3f;
public float fleeSpeed = 4f;
[Header("HP")]
public float maxHp = 100f;
public float hp = 100f;
public float lowHpThreshold = 30f;
// ★ Dictionary: 상태 이름 → 색상 매핑 (세션6)
Dictionary<string, Color> stateColors;
// ★ Stack: 상태 전환 히스토리 (세션2)
Stack<string> stateHistory = new Stack<string>();
// 상태 전환 이벤트 — UI 테스트에서 구독
public System.Action<string, string> onStateChanged;
SpriteRenderer sr;
void Start()
{
sr = GetComponent<SpriteRenderer>();
hp = maxHp;
// Dictionary 초기화
stateColors = new Dictionary<string, Color>()
{
{ "Idle", Color.white },
{ "Chase", Color.yellow },
{ "Attack", Color.red },
{ "Flee", new Color(0.3f, 0.5f, 1f) },
{ "Patrol", new Color(0.5f, 1f, 0.5f) }
};
ChangeState(idleState);
}
void Update()
{
if (player == null || currentState == null) return;
// ★★★ switch 없이 다형성으로! ★★★
currentState.Update(this);
}
// ── 상태 전환 ──────────────────────────────────────────────
public void ChangeState(IEnemyState newState)
{
if (currentState == newState) return;
string oldName = currentState?.Name ?? "None";
// ★ Stack에 이전 상태 기록
if (currentState != null)
stateHistory.Push(currentState.Name);
currentState?.Exit(this);
currentState = newState;
currentState.Enter(this);
onStateChanged?.Invoke(oldName, currentState.Name);
Debug.Log($"[StatePattern] {oldName} → {currentState.Name}");
}
// ── 헬퍼 메서드 (상태 클래스에서 호출) ────────────────────
public void SetColor(Color color)
{
if (sr != null) sr.color = color;
}
public void MoveToward(Vector3 target, float speed)
{
Vector2 dir = ((Vector2)target - (Vector2)transform.position).normalized;
transform.position += (Vector3)(dir * speed * Time.deltaTime);
}
public float DistToPlayer()
{
if (player == null) return float.MaxValue;
return Vector2.Distance(transform.position, player.position);
}
// ── HP 조작 ────────────────────────────────────────────────
public void TakeDamage(float amount)
{
hp = Mathf.Max(0, hp - amount);
}
public void Heal(float amount)
{
hp = Mathf.Min(maxHp, hp + amount);
if (currentState == fleeState && hp > lowHpThreshold)
ChangeState(idleState);
}
// ── 외부 접근용 ────────────────────────────────────────────
public string CurrentStateName => currentState?.Name ?? "None";
public Stack<string> GetStateHistory() => stateHistory;
public Dictionary<string, Color> GetStateColors() => stateColors;
}
상태 전환, 그리고 각 상태에서 사용할 메서드가 이 코드에 있다.
상태 인스턴트는 메모리에 올려두고 시작한다.
그리고 상태 인스턴트
using UnityEngine;
public class IdleState : IEnemyState
{
public string Name => "Idle";
public void Enter(EnemyController e)
{
e.SetColor(Color.white);
}
public void Update(EnemyController e)
{
// 플레이어가 추적 범위 안에 들어오면 Chase로 전환
if (e.DistToPlayer() < e.chaseRange)
e.ChangeState(e.chaseState);
}
public void Exit(EnemyController e) { }
}
// ─────────────────────────────────────────────
// 2. ChaseState — 추적 (노란색)
// ─────────────────────────────────────────────
public class ChaseState : IEnemyState
{
public string Name => "Chase";
public void Enter(EnemyController e)
{
e.SetColor(Color.yellow);
}
public void Update(EnemyController e)
{
// 플레이어를 향해 이동
e.MoveToward(e.player.position, e.moveSpeed);
if (e.DistToPlayer() < e.attackRange)
e.ChangeState(e.attackState); // 사정거리 → 공격
else if (e.DistToPlayer() > e.chaseRange)
e.ChangeState(e.idleState); // 너무 멀면 → 대기
}
public void Exit(EnemyController e) { }
}
// ─────────────────────────────────────────────
// 3. AttackState — 공격 (빨간색)
// ─────────────────────────────────────────────
public class AttackState : IEnemyState
{
public string Name => "Attack";
public void Enter(EnemyController e)
{
e.SetColor(Color.red);
}
public void Update(EnemyController e)
{
if (e.hp <= e.lowHpThreshold)
e.ChangeState(e.fleeState); // HP 낮으면 → 도망
else if (e.DistToPlayer() > e.attackRange)
e.ChangeState(e.chaseState); // 멀어지면 → 다시 추적
}
public void Exit(EnemyController e) { }
}
// ─────────────────────────────────────────────
// 4. FleeState — 도망 (하늘색)
// ─────────────────────────────────────────────
public class FleeState : IEnemyState
{
public string Name => "Flee";
public void Enter(EnemyController e)
{
e.SetColor(new Color(0.3f, 0.5f, 1f));
}
public void Update(EnemyController e)
{
// 플레이어 반대 방향으로 이동
Vector2 fleeDir = ((Vector2)e.transform.position - (Vector2)e.player.position).normalized;
e.transform.position += (Vector3)(fleeDir * e.fleeSpeed * Time.deltaTime);
// 충분히 멀어지면 HP 회복 후 대기
if (e.DistToPlayer() > e.fleeDistance)
{
e.hp = e.maxHp;
e.ChangeState(e.idleState);
}
}
public void Exit(EnemyController e) { }
}
// ─────────────────────────────────────────────
// 5. PatrolState — 순찰 (연두색) ★ OCP 시연용
// 기존 파일(IdleState~FleeState, EnemyController) 수정 없이 추가!
// ─────────────────────────────────────────────
public class PatrolState : IEnemyState
{
public string Name => "Patrol";
Vector2 startPos;
float angle = 0f;
const float patrolRadius = 3f;
public void Enter(EnemyController e)
{
startPos = e.transform.position; // 현재 위치를 순찰 중심으로
angle = 0f;
e.SetColor(new Color(0.5f, 1f, 0.5f)); // 연두색
}
public void Update(EnemyController e)
{
// 원형 순찰 (중심점 기준으로 빙글빙글)
angle += e.moveSpeed * 0.5f * Time.deltaTime;
Vector2 target = startPos + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * patrolRadius;
e.MoveToward(target, e.moveSpeed * 0.7f);
// 플레이어 발견 → 즉시 추적
if (e.DistToPlayer() < e.chaseRange)
e.ChangeState(e.chaseState);
}
public void Exit(EnemyController e) { }
}
인터페이스를 상속받은 클래스는 내부에서 switch의 각 case처럼 조건 분기, 메서드를 사용한다.
정확히 똑같이 기능한다.
2. Delegate
어제 배운 Acition과 연관이 아주아주 깊은 코드.
using UnityEngine;
public class DelegateExample : MonoBehaviour
{
public delegate void MyDelegate(int number);
void PrintNumber(int number)
{
Debug.Log("Number: " + number);
}
void Start()
{
MyDelegate myDel = PrintNumber;
myDel(10);
}
}
Aciton처럼 메서드를 담고 매개변수를 받아 작동시킬 수 있다.
개쩌는 점이라면 메서드를 합칠(?)수 있다는 것!
using UnityEngine;
// 멀티캐스트 델리게이트
public class DelegateExample2 : MonoBehaviour
{
public delegate void MyDelegate();
void Start()
{
MyDelegate myDel = Hello;
myDel += Bye;
myDel -= Bye;
myDel();
}
void Hello()
{
print("Hello");
}
void Bye()
{
print("Bye");
}
}
연산자를 통해 합치고 뺄 수 있다.
합치는 경우 둘 다 작동시킨다.
using System;
using UnityEngine;
// 멀티캐스트 델리게이트
public class DelegateExample3 : MonoBehaviour
{
void Start()
{
Action myAction = Hello;
myAction();
}
void Hello()
{
Debug.Log("Action Hello");
}
}
액션도 비슷한데, 이미 만들어진 delegate라 " delegate void MyDel" 이런 선언이 필요없고
반환값이 없는 메서드 전용이다. 빠르고 간결하게 쓸 수 있다는 장점이 있음.
사용 예
using System;
using UnityEngine;
public class PlayerAttack : MonoBehaviour
{
public static Action OnAttack;
void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
Attack();
}
}
void Attack()
{
Debug.Log("플레이어 공격");
OnAttack?.Invoke();
}
}
Action OnAttack을 전역으로 선언한다.
using UnityEngine;
public class AttackEffect : MonoBehaviour
{
// MonoBehaviour가 활성화될 때 1회 실행
void OnEnable()
{
PlayerAttack.OnAttack += PlayEffect; // 구독
}
void OnDisable()
{
PlayerAttack.OnAttack -= PlayEffect;
}
public void PlayEffect()
{
Debug.Log("이펙트 재생");
}
}
using UnityEngine;
public class AttackSound : MonoBehaviour
{
private void Onable()
{
PlayerAttack.OnAttack += PlaySound;
}
private void ODisable()
{
PlayerAttack.OnAttack -= PlaySound;
}
public void PlaySound()
{
Debug.Log("사운드 재생");
}
}
그리고 OnAttack이 실행될 때 함께 작동할 메서드들은 OnEnable 즉 MonoBehaviour이 생성되는 시점에 "구독"을 한다.
이렇게 Action에 추가된 메서드들은 메모리에서 파괴되는 시점(OnDisable)에 구독 해제하고 사라진다...
추가로 Event라는 거로 제어할 수도 있다.
using UnityEngine;
using UnityEngine.Events;
// Unity Event
public class PlayerAttack2 : MonoBehaviour
{
[Header("공격 이벤트")]
public UnityEvent EAttack; // 공격 실행될 이벤트
void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
Attack();
}
}
void Attack()
{
Debug.Log("플레이어 공격");
EAttack?.Invoke();
}
}

그럼 이벤트가 이런 식으로 뜨는데, 여기에 버튼이벤트 달아주는 것처럼 넣어주면 된다.
동일하게 작동함.

3. 2D Light
Universial 2D로 생성한다.
그리고 ProjectSetting > Graph에서

RP선택한다.
그리고 Quality

이거도 RP로 설정한다.

Light > Global Light 2D 하나 생성해 봅시다.


Intensity를 조절하면 밝기를 조절할 수 있다.
색상도 조절 가능.
Spot Light 2D는 한 지점에 빛을 쏘는 것임.
기본적으로 원형으로 빛이 나오는데,

inner / outer Spot 조절하고 이것저것 하면

손전등도 만들 수 있다!
4. Shader Graphs
셰이더 그래프를 이용하면 머터리얼에 다양한 효과를 줄 수도 있고, 자원 조절을 할 수도 있다.

Create > Shader Graph > UPR > Sprite Lit Shader Graph

만들어진 셰이더 그래프를 에디터에서 확인한다.
뭐 요소가 겁나 많은데 이것저것 세팅한다.

대충 Glow를 구현했다.

메인 카메라에서 포스트 프로세싱 체크

글로벌 볼륨 추가,


Bloom 추가. Intensity 조절

만들어둔 Glow Shader Graph우클릭을 해서 Material을 생성하면 Glow가 적용된 머터리얼이 생성된다.
이걸 오브젝트에 적용해 보자.


컬러의 Intensity, Glow Amount를 조절하면
빛을 조절할 수 있다.

굿
===========================================================
State Machine을 가독성 좋게 사용할 수 있게 되었다 !
Delegate를 사용할 수 있게 되었다 !
Light, Shader Graph를 사용할 수 있게 되었다 !

'Unity Engine' 카테고리의 다른 글
| [멋쟁이사자처럼부트캠프 유니티 게임 개발 7기] 2026년 3월 6일 회고록 - 멀티 께임 구현 (0) | 2026.03.06 |
|---|---|
| [멋쟁이사자처럼부트캠프 유니티 게임 개발 7기] 2026년 2월 25일 회고록 - 3D 입문 (0) | 2026.02.25 |
| [멋쟁이사자처럼부트캠프 유니티 게임 개발 7기] 2026년 2월 23일 회고록 - State Machine(FSM), 구글 플레이 스토어 출시 및 애드몹 (0) | 2026.02.23 |
| 유니티 세팅 (0) | 2026.02.11 |
| [멋쟁이사자처럼부트캠프 유니티 게임 개발 7기] 2026년 2월 10일 회고록 - Stack, Queue, Tree 그리고 enum (0) | 2026.02.10 |