Game Dev Study Log

by ricale

취미로 하는 게임 개발 공부

3D 강의 코드를 2D 프로젝트로 옮기기 6

한동안 공과 사 양쪽이 바빠서 정신 없었다. 다시 시작, 할 수 있을런지?

일단 기록하지 않았던 기간동안 파편적으로 진행한 것까지 합쳐서 오늘까지 Pathfinding 쪽과 Unit 쪽 코드를 모두 가져왔다.

Unit 을 가져오는 과정에서 HealthSystemTurnSystem 도 같이 가져왔다.

3D 강의 코드를 2D 프로젝트로 옮기기 4

grid debug object 스트립트 및 prefabs 들을 만들어서 게임 화면에 해당 객체들을 찍어보려고 시도 중이다.

2D Object 에는 TextMesh Pro 가 없어서 당황해서 삽질을 하다가, 나중에야 2D 프로젝트에서도 3D Object - TextMesh Pro 를 사용한다는 걸 알아내서 해당 객체로 prefab 을 만들었다.

스크립트까지 다 작성한 다음에 적용해보았는데 제대로 적용되지 않아서 살펴보니, gridPosition 에서 x, z 좌표를 사용했는데 2D 프로젝트에서는 x, y 좌표를 사용해야 해서 수정이 필요함을 확인했다.

3D 강의 코드를 2D 프로젝트로 옮기기 2

오늘은 LevelGrid 작성을 완료하고 Pathfinding 작성을 시작했다. Pathfinding 작성에 앞서 PathNode 작성도 필요했기에 해당 클래스는 작성을 완료했다.

생각보다 고개를 갸우뚱 하는 코드들이 좀 있었다. 예를 들면 불필요한 코드 중복이라던가, LevelGridPathfinding 의 순환참조라던가. 하지만 지금 목표는 리팩토링이 아니라 코드 리뷰 및 리마인드 이므로, 되도록 코드를 수정하지는 않고 있다.

2D 강의 영상 종료 및 구현 재시작

# 1

강의: Unity Tutorial 2024 - Build a 2D RPG

강의 Part 09 내용은 그다지 중요해보이지 않아서 바로 다음 강의로 넘어갔다.

Part 10 내용은 배틀 시스템 구현이었는데, 초반 몇 분을 보니 도저히 강의 시간 내에 (이전 강의에서 몇 번 언급했던) 파이널판타지나 포켓몬 스타일의 배틀 시스템을 구현할 수 없을 것 같아서 뒷부분으로 돌려보니 역시나 밑작업만 완료하는 정도였다.

이어지는 강의가 있어야겠지만 이 강의가 올라온 것이 2023년이고, 후속 강의는 그 이후에 올라오지 않고 있다. (이 유튜브 채널에는 이 강의 영상 10개만 있고 이외에는 아무것도 없다.)

일단 2D 게임 객체 생성의 기초는 대략 본 것 같으니 2D 강의는 여기까지만 보도록 하겠다.

강의에 대한 총평을 하자면, 무료 강의인 것을 감안하면 나쁘지 않았다, 정도로 하겠다.

# 2

다시 습작 만들기를 시작하기 위해 Unity 2D 프로젝트를 새로 만들었다.

TrialAndError 라는 이름을 다시 쓰기에는 맥빠지므로 IdKwit 정도로 만들었다.

일단은 이전 3D 강의에서 구현했던 코드를 가져오는 게 좋겠다 싶어서 3D 강의 때 따라한 프로젝트를 열었는데, 생각보다 코드가 많아서 어떻게 옮길까 고민을 좀 했다.

생각해보니 그 때 강의가 끝나고 전체적인 코드 리뷰를 한다고 말만하고 코드 리뷰를 하지 않았었다. 그래서 코드리뷰도 할 겸 코드를 그냥 복사하지 않고 따라서 다시 쳐보면서 옮기기로 결정했다.

시작은 LevelGrid 부터다. LevelGrid 를 작성하다보니 GridSystem, GridObject, GridPosition 내용이 필요한 것 같아서 해당 내용들을 먼저 작성했고, 덩치가 커보이는 Unit, IInteractable, Pathfinding 클래스 생성만 해두고 본 내용은 아직 작성하지 않았다.

  • 작성 완료: GridSystem, GridObject, GridPosition
  • 작성 중: LevelGrid
  • 껍데기만: Unit, IInteractable, Pathfinding

2D 강의 영상 Part 7,8,9 시청

강의: Unity Tutorial 2024 - Build a 2D RPG

Part 07 Adding Dialog 1 (14:45~, 27:36)

DialogManager 클래스를 작성, 싱글톤으로 만든 후 ShowDialog 메서드 작성. ShowDialog 에서 대화 내용을 주고 받을 때 사용하기 위한 데이터 객체용 클래스 Dialog 도 생성. NPCControllerInteract 메서드에서 DialogManager.ShowDialog() 를 실행하게끔 한다.

NPC 에게 말을 걸 경우 말풍선까지 잘 뜨지만, 다음 문장으로 넘어가지 않고 대화중인데도 움직일 수 있는 등 미비된 사항들이 있다. 이건 다음 영상에서 마저 구현한다.

Part 08 Adding Dialog 2 (14:05)

DialogManagerOnShowDialog, OnHideDialog 이벤트를 추가하고, 해당 이벤트리스너를 통해 GameController 가 게임 상태를 바꾸게 했다.

GameController 가 대화 상태일 때는 DialogManager.HandleUpdate() 만 실행함으로써 말풍선 이외에는 업데이트가 되지 않게 제어한다.

DialogManager.HandleUpdate() 는 coroutine 을 활용해 라인 별로 타이핑하듯이 대화를 출력하는 기능을 구현한다.

coroutine.. 분명히 알던 개념인데 오랜만에 봐서 그런지 엄청 헷갈린다. 개념을 다시 찾아봐야겠다.

Part 09 Battle System Design (~04:30, 28:42)

전투 시스템 설계에 대한 이론 수업이라고 해서 들어야 하나 고민 중. 아마 높은 확률로 듣지 않고 다음 강의로 넘어갈 듯.

2D 강의 영상 Part 6,7 시청

강의: Unity Tutorial 2024 - Build a 2D RPG

Part 06 Interfaces (14:33)

Interactable 인터페이스를 구현해서 NPCController, ChallengerController 클래스를 만들고 각 객체에 붙여보았다.

Part 07 Adding Dialog 1 (~14:45, 27:36)

게임 전반을 제어하기 위한 GameController 클래스를 추가. 대화 상자 용으로 사용될 UI 도 추가. 남은 부분에서 말을 걸면 대화상자가 열리는 것을 구현할 듯.

2D 강의 영상 Part 4,5 시청

강의: Unity Tutorial 2024 - Build a 2D RPG

Part 04 Collisions (16:34)

Sorting Layer 는 일반 Layer 와 달리 z-index 역할.

2D 장애물을 만들기 위해서는 별도의 TileMap 을 만들고 그 위에 Tile Palette 로 그림을 그린 다음, Tilemap Collider 2D, Composite Collider 2D (+ Rigidbody 2D) 를 적용.

  • 중력의 적용을 없애려면 Rigidbody 2D 의 Body Type 을 Static 으로
  • Tilemap Collider 2D 에서는 Used By Composite 을 체크해서 Composite Collider 2D 를 자동 사용하도록 적용
  • 이 시점에서 Tile Palette 로 TileMap 위에 그린 그림에 자동으로 collider 가 적용된 것을 Scene view 에서 확인 가능.
    • 영상에서는 Composite Collider 2D 의 Gemometry Type 을 Polygons 로 바꾸라는 데 뭐가 바뀌는 건지는 잘 모르겠다.

지금까지 강의에서 작성한 코드량이 얼마 되지도 않지만, [SerializeField] private 대신 public 을 사용하는 것을 보니 코드 퀄리티에서는 배울 게 없을 것 같다. 하긴 설명하는 수준을 보면 초심자용 영상인 것은 확실하다.

Part 05 Adding NPCs (~09:40, 17:59)

Project Settings 의 Graphics - Camera Settings 에서

  • Transparency Sort Mode - Custom Axis
  • Transparency Sort Axis - x: 0, y: 1, z: 0

으로 하면 y 좌표 기준으로 같은 Layer Sprite 객체가 정렬된다.

새로운 캐릭터를 추가하고 Interactable 이라는 Layer 를 추가한 뒤 해당 캐릭터에 지정해줬다. 이후에는 캐릭터에게 말을 걸면 말풍선이 나오는 것을 구현할 듯.

2D 강의 영상 Part 3 시청

강의: Unity Tutorial 2024 - Build a 2D RPG

Part 03 Character animation (25:44)

애니메이션을 생성하고 적용하는 방법이 이전 강의와는 다르다. 2D 라서, 라기보다는 애니메이션을 적용할 수 있는 방법이 많은 것이라고 이해하는 것이 좋을 것 같다.

2D 강의 영상 Part 2 시청

강의: Unity Tutorial 2024 - Build a 2D RPG

Part 02 Character movement (13:04~, 27:39)

움직임을 마저 구현. 입력을 받는 방법이 이전 강의와 다르다.

그리고 coroutine 을 사용하는데, 지금 시점에서는 굳이 coroutine 을 사용할 필요가 없어서 의아하긴 하다. 후속 영상에서 coroutine 을 사용한 개선이 있으려나?

2D 강의 영상 Part 2 시청 중

강의: Unity Tutorial 2024 - Build a 2D RPG

강의 사이트가 아니라 유튜브 영상이다 보니 "강의 수강" 이라는 표현이 적절할지 헷갈린다.

Part 02 Character movement (~13:04, 27:39)

캐릭터의 움직임을 구현하는 강의인데, 따라하던 중 갑자기 맥북의 드래그앤드랍이 먹통이 된다거나, 그래서 재부팅했더니 이번에는 vscode 의 dotnet 연동이 먹통이 된다거나 하는 이슈들이 있었다. 덕분에 예상치 못한 시간이 소모되었다.

강의는 내용이 너무 기초적이라서 어느정도 스킵할까 싶었지만 혹시라도 몰랐던 내용을 놓칠까봐 그러지 못하고 있다. 그 와중에 유튜버의 말이 너무 느려서 영상 배속도 조정해보고 있다.

여튼 이러저러한 이슈들 때문에 집중력이 좀 떨어져 많이 진행하지 못했다. 하지만 일단 언제나 그렇듯 꾸역꾸역, 느리더라도 어떻게든 진행하는 게 중요할 것이다. 열심히 해보자.

습작 프로젝트를 2D 로 전환, 그에 따른 강의 선택.

<3D 에서 2D 로 방향 전환>

주말간 너무나 파편화된 시간밖에 쓰지 못해서 차마 글을 쓰지는 못했다. 여튼 습작 프로젝트는 클래스 별로 Prefab 을 몇 개 추가하고 스탯 기준 턴 시스템까지 구현한 상태였다. 하지만 어제 willow 형과의 논의에서 습작을 3D 가 아닌 2D 로 만들자는 결정이 나서 2D 에 대한 학습이 필요해졌다. 따라서 습작 프로젝트 진행은 (당연히) 멈추고, 관련 강의를 들어야 할지 아니면 무작정 2D 프로젝트를 만들면서 구글링 이나 생성형 AI 에 의지해야 할지 고민을 좀 했다.

<2D 강의 찾기>

결론은 강의를 듣긴 들어야 겠다는 것.

하지만 Udemy 에서 검색했을 때는 그다지 원하는 강의가 나오지 않았다. 그래서 유튜브에서 검색을 좀 해보았는데, 결국 찾은 것이 EPIC Game Dev 라는 채널의 Unity Tutorial 2024 - Build a 2D RPG 라는 재생 목록이다. 총 10개의 영상으로 구성되어 있고 다 합쳐서 3~4시간 정도 될 것 같다.

<강의 맛보기, Part 01 Adding Background (12:02)>

일단 첫 번째 영상을 보았는데, Sprite 타일 맵을 리소스로 어떻게 추가하고, 추가한 타일 맵으로 일종의 브러시를 만들어서 어떻게 맵을 그릴 수 있는지에 대한 내용이었다. 정제된 강의라기 보다는 날것의, 유튜브 다운 영상이었으나 어렵거나 산만하지 않아서 편하게 볼 수는 있을 것 같다.

그래서 일단 먼저 이 영상을 다 챙겨본 뒤에, 다시 습작 프로젝트로 돌아갈 예정이다.

클래스 별 Prefab 추가 중 2

시간을 많이 못 썼다. 일단 Prefab 을 추가 중이다. 각각의 클래스를 구분하기 위해 각각의 2d sprite 와 애니메이션도 같이 붙이고 있다.

클래스 별 Prefab 추가 중

<임시로 사용할 어셋 탐색 및 추가>

클래스 별로 Prefab 을 만들어두려고 하는데, 다 같은 리소스면 재미가 없으므로 무료 2D Sprite 어셋을 좀 찾아봤다. https://craftpix.net/ 여기에서 대략 SD 캐릭터 느낌이 나는 무료 어셋들을 찾았다. 우리 클래스가 민병/경보병/중보병/궁수 이런 느낌인데 딱 맞는 무료 어셋은 없어서 아쉬움이 좀 있지만, 임시 어셋이니 적당히 하기로 했다.

오늘은 어셋을 적용하다가 끝났다. 내일 마저 적용하고 다음 스탭으로 넘어가야겠다.

<클래스 구분>

처음에는 Unit 클래스를 상속받아서 뭔가를 만들어야 하나 싶었는데, 일단은 Prefab 별로 스탯 적용이 가능한 상태이므로 그럴 필요 없을 것 같다. 근거리/원거리 공격도 일단 강의 수강하면서 구현은 했으므로 문제 없을 듯.

내일 서너 클래스 정도는 만들어서 배치해보고, 그 다음에는 직접 컨트롤해볼 수 있게 할 예정이다.

지금도 직접 컨트롤은 되지만 적 캐릭터는 최소한의 로직으로 자동 컨트롤되는데, 이 부분을 임시로 빼 둘 생각이다.

<조금만 했어도 기록하자>

오늘 시간을 많이 쓰지 못했고 진행도 많이 하지 못해서 기록을 건너뛸까 했는데, 그런 것도 기록하면서 하려고 만든 게 이 블로그라는 걸 깨달았다.

이 블로그는 다른 사람에게 보여주려고 하기보다는 공부 및 개발을 계속 해나가는 원동력으로 쓰기 위해 만든거니까 (그래서 댓글이나 GA 나 기타 등등은 우선순위가 낮아서 아무것도 달지 않았다), 아무리 조금만 했어도 웬만하면 기록하는 걸로 하자.

스탯 기반 턴 시스템 구현

<스탯 관리 구조 고민 및 구현>

어제도 시간을 쓰긴 했다. 다만 사용한 시간 중 대부분을 클래스를 어떻게 구성할 것인가에 대한 고민에 투자했다. 스탯 기반으로 턴이 돌아가게 하려면 스탯을 관리해야 할텐데 어디서 관리할까? 가 주요 고민이었다.

어제 생각한 건 "단순히 스탯을 Unit 클래스에 넣는 걸로는 장기적으로 관리가 어려울 것 같은데 어떻게 하면 좋을까?" 였고, 오늘까지 고민한 끝에 아래처럼 구성하기로 결론을 내렸다.

  • UnitStatus: 캐릭터의 스탯에 관련된 것들을 종합적으로 관리, 아래 세 클래스의 인스턴스를 갖고 있다.
    • UnitAttributes: 힘, 민첩 등 캐릭터의 순수 스탯을 관리. 레벨 업 등으로 인해 영구적으로 변경되는 사항도 관리한다.
    • UnitProgression: 캐릭터의 레벨과 경험치를 관리. 레벨업 할 때의 이벤트를 추가해서 UnitStatus 에서 그 이벤트를 구독하면서 UnitAttributes 에 영향을 줄 예정
    • UnitEquipment: 캐릭터의 장비와 그에 따른 능력치 변경을 관리
    • UnitEffect: 캐릭터의 버프 및 디버프와 그에 따른 능력치 변경을 관리

결국 Unit 에서는 UnitStatus 에만 접근해 원하는 능력치를 얻어낼 예정이다. 예를 들어 이번에 UnitStatus 에 추가한 GetMovementPriority 는 하위 클래스 인스턴스에서 필요한 값을 뽑아와서 턴 우선순위 값을 반환하는 메서드다.

<스탯 기반 턴 시스템 적용>

위에서 추가한 UnitStatus.GetMovementPriority 를 사용해 TurnSystem 내부에 있는 unitList 를 정렬하고, 정렬된 순서대로 턴이 진행되게끔 구현.

턴이 바뀔 때마다 캐릭터의 선택도 자동으로 되게끔 했다.

<다음 스탭>

다음 스탭은 턴이 바뀔 때 해당 캐릭터로 카메라가 이동하도록 하는 것. 그걸로 턴제 시스템은 한 번 일단락 지은 뒤 클래스 구분 및 기본 스탯 적용으로 넘어갈 예정이다. 이미 UnitStatus 및 하위 클래스들의 틀을 잡아놓았으므로, 어려울 것 같지는 않다.

구현(=삽질) 시작

<프로젝트 TrialAndError>

어제 2D Sprite 적용을 시도해보면서 만들었던 프로젝트의 이름은 TrialAndError. 강의 81강까지 들으며 따라했던 코드를 그대로 복사해서 새로 만든 프로젝트다. 당분간은 이 프로젝트에서 계속 시행착오를 반복하며 연습 겸 습작 구현은 진행할 예정이다.

어젯밤에 willow 형과 앞으로의 구현 방향을 간단히 논의했다. 형이 벤치마킹 하려는 게임은 파이어엠블렘 시리즈이고, 해당 시리즈를 기준으 하면서도 당장 내 역량으로 구현이 가능한 최소 수준의 게임을 구현해 볼 예정이다.

그래서 당장 이야기가 나온 태스크는

  • 스탯 기반 턴 시스템 구현
  • 캐릭터의 클래스 분리 (현재는 Unit 하나로 통일되어 있음)
    • 클래스 간 차이는 당장은 스탯만 다르게, 그리고 이후에는 각각의 클래스가 취할 수 있는 액션에도 차이를 둔다.

정도이다.

<스탯 기반 턴 시스템 구현 시작>

강의에서 구현한 턴 시스템은 적군과 아군이 번갈아가면서 진행하는, 아주 단순한 구조다. 하지만 요구사항은 캐릭터마다 존재하는 스탯에 의한 우선순위 결정 후 캐릭터마다 턴을 진행하는 것이기 때문에 개선이 필요하다.

그래서 오늘은 TurnSystem 에서 unitList 를 관리하게 하고, 모든 Unit 은 초기화되는 순간 TurnSystem.Instance.AddUnit() 함수를 통해 자신을 등록하게 했다. 그래고 TurnSystem 내부에서는 "현재 액션이 가능한 유닛" 을 unitList 의 인덱스로 관리하게끔 했다. 이제 턴 종료 및 시작 시마다 캐릭터 단위로 액션 포인트가 적용된다.

아직 캐릭터 스탯에 의해 턴 순서가 정해지진 않는다. 그리고 턴이 시작된 캐릭터에게 카메라 포커싱을 주고 선택도 자동으로 되게끔 해야 할 것 같다.

강의 77~81강 수강, 강의 감상, 이후 어떻게 할지, 그리고 2D Sprite 적용

#1

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

77. Interface IInteractable (12:06)

이전 강의에서 예고한대로, 이번 강의에서 상호작용 가능한 객체 클래스를 추상화한다.

이전 강의에서 만든 Door 와, 이번 강의에서 테스트를 위해 추가한 InteractSphere 두 클래스에서 공통으로 쓰일 함수 InteractIInteractable 이라는 인터페이스를 만들어서 추상화한 뒤에 사용하게끔 코드를 수정했다.

그리고 이전에 73,74강에서 의문을 가졌던, 파괴 가능한 객체를 왜 추상화하지 않는가에 대한 대답이 나왔는데, 강의 마지막에 당연히 해당 객체 클래스들도 추상화 가능하고 그걸 도전해보라고 말하고 있다.

78. Input Refactoring (08:49)

기존에 Unity 에서 제공하는 Input 클래스를 필요한 곳에서 각각 사용하고 있던 것을, InputManager 라는 새로 만든 클래스에서 모두 관리하게끔 수정. 다른 클래스들은 이제 입력 관련 기능이 필요할 때는 InputManager 를 거치게 된다. 간단한 리팩토링이었고 다음 강의에서 입력 시스템을 개선한다고 한다.

79. Input System (15:42)

Package Manager 에서 Input System 설치. 설치 직후 기존 Input Manager 를 비활성화하고 새로운 것을 활성화하겠냐는 대화 상자가 뜨는데 일단은 No.

이후 Edit - Project Settings - Player 에서 Active Input Handling 을 찾아 Both 를 선택.

screenshot

Project 탭에서 Input Actions 를 PlayerInputActions 를 만들고, 더블 클릭해서 수정창을 열어서 입력 값들을 설정해준다.

screenshot

그리고 PlayerInputActions 의 c# 클래스를 자동 생성하게끔 설정하고

screenshot

InputManager 클래스에서 해당 클래스를 사용하면 된다.

        playerInputActions = new PlayerInputActions();
        playerInputActions.Player.Enable();
    }

    public Vector2 GetMouseScreenPosition()
    {
#if USE_NEW_INPUT_SYSTEM
        return Mouse.current.position.ReadValue();
#else
        return Input.mousePosition;
#endif
    }

키보드로 카메라 이동/회전/확대축소 도 설정하는 코드가 있는데 궁금하면 강의를 다시 보거나 코드를 다시 보자.

강사가 preprocessor 까지 써가면서 Input 을 사용하는 기존 코드를 남겨놓는 이유는 Input 이 테스트 등의 용도에서는 작성 및 사용이 훨씬 간단해서라고 한다. 즉 간단한 프로토타이핑이나 테스트에는 기존 Input 을 사용하고, 본격적인 구현에서 Input System 을 사용하라는 의미다.

이건 나중에 상황 봐서 걷어내도 될 것 같고, 유용하다면 계속 써도 될 것 같다. 너무 신경쓰진 말자.

80. Final Level (04:18)

맵을 좀 더 수정하고 상호 작용 가능한 객체 종류를 추가하고 문이 열리기 전에는 닫혀 있는 곳을 검은 안개?로 보이지 않게 처리하는 등 실제로 한 스테이지를 플레이해볼 수 있는 처리들을 진행했다.

하지만 지금까지의 강의들과는 다르게 작업하는 모습을 빨리 감기로 하이라이트만 보여주는 모습이었는데, 마지막으로 과제를 준 느낌이다. (날먹은 아닌 것이 강의 GitLab 에 들어가면 해당 강의의 코드 내역이 모두 존재한다.)

81. Congratulations! What’s Next? (01:41)

마무리 인사.

#2

<강의 리뷰>

보너스 섹션이 두 섹션 남아있지만 본 강의가 끝난 시점에서 가벼운 리뷰를 한 번 해보자.

일단 단순히 동작하는 코드 혹은 결과를 만드는 것이 아니라 확장 및 유지보수에 용이한 코드 및 결과 구현에 집중한 것이 매우 마음에 드는 강의였다.

하지만 몇몇 기초적인 부분이 누락된 것이 아쉬운데 예를 들면 씬 전환과 같은 기본적인 게임 구성에 필요한 것들이다. 이건 쉽게 찾아볼 수 있는 정보라 그냥 강의에서 뺀 것일까 (아니면 UI 컴포넌트로 충분히 구성 가능하다고 생각한 것일까). 이것만 살짝 아쉽다.

전체적으로 보면 매우 만족스러운 강의였고 (리뷰도 만점 줬다.) 보너스 섹션인 Hex Grid System 과 Multi-Floors 도 (당장은 아니더라도) 모두 볼 생각이다.

<앞으로 어떻게 할까>

일단 (같이 게임을 만들기로 의기투합한) willow 형이 원하는 게임은 캐릭터들이 2D 이므로, 3D 맵에서 2D 스프라이트가 움직이게 하던, 혹은 아예 2D 로 통째로 프로젝트를 만들던, 게임 만들기에 앞서 추가적인 지식이 필요한 상황이다.

이 부분을 1. 그냥 시행착오로 부딪혀 볼 것인지, 2. 혹은 다른 강의를 들을 것인지, 3. 혹은 ChatGPT 같은 생성형 AI 의 도움을 받을 것인지 고민 중에 있다. 당장은 1,3번을 조합하되 어려움을 느끼면 2번을 고려할 생각이다.

3D 로 게임을 만드는 것에도 여전히 흥미는 있기 때문에, 이 흥미는 지금이든 나중이든 어떤 식으로 발현해나갈 것인지도 고민이다.

<2D Sprite 적용해보기>

내친 김에 2D Sprite 이미지를 적용하는 걸 좀 진행해봤다. 움직일 때 캐릭터는 회전하지 않게 하고, 카메라 회전 시 항상 2D 이미지가 잘 보이도록 회전하게 했다. 애니메이션은 Idle, Walking, Slashing 세 개 정도만 적용했고, 걷거나 공격할 때 캐릭터가 해당 방향을 바라보도록 (flipX) 적용했다. 생각보다 쉽게 잘 된다.

강의 75강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

75. Sword Action (18:29)

SwordAction 을 추가. 전에 했던 GrenadeAction 추가와 방식은 거의 같았다. 다만 좀 다른 것은 검을 휘두르는 액션을 추가하고 무기 prefab 을 추가한 것 정도.

뭔가 무기를 더 잘 관리할 수 있는 방법에 대해 말로 소개해줬는데, 이번에는 그 방법을 사용하지 않고 가장 간단한 방법을 사용. UnitAnimator 에 총과 칼 prefab 각각 등록해서 사용한다.

강의 74강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

74. Destructible Crate Parts (12:03)

Package Manager 에서 ProBuilder 찾아서 설치.

Crate prefab 을 복사해 CrateDestroyed prefab 추가. 내부의 실제 상자 오브젝트를 선택한 뒤, Tools - ProBuilder - ProBuilderize 를 선택.

그 후 ProBuilder 의 Experimental 을 enabled 한 뒤에 Experimental 의 Boolean Tool 을 사용해야 한다. 자세한 사용법이 생각나지 않으면 강의를 다시 보자.

Boolean Tool 을 사용해 Crate 를 조각낸 뒤, 조각낸 객체들에 MeshColliderRigidBody 컴포넌트를 부착하자. 그 다음 폭발 후 날아가는 애니메이션은 UnitRagdoll 에서 했던 것처럼 각 RigidBodyAddExplosionForce 를 해주면 된다.

강의 71,72,73강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

71. Grenade Action (13:09)

GrenateAction 추가.

BaseAction 기반의 새로운 액션은 어떻게 추가하는지 복습하는 동시에, 발사체가 있고 범위 데미지가 있는 액션은 어떻게 구현하는지에 대한 튜토리얼이라 할 수 있겠다.

다만 아쉬운 건 수류탄이 직선으로 linear 타이밍으로 날아간다는 것과 폭발 이펙트가 없다는 것인데, 폭발 이펙트는 다음 강의에서 구현한다. 날아가는 애니메이션은 수정할런지.

72. Grenade Visuals (16:59)

폭발 이펙트는 이전의 ShootAction 에서 사용했던 BulletHitVFX 를 복사해서 수정.

  • Shape - Radius: 0.2 -> 2
  • Emission - Brusts
    • Count: 30 -> 200
    • Cycles: 1 -> 2
    • Interval: 0.010 -> 0.050
  • Start Speed: 0.5 ~ 3 -> 3 ~ 10
  • Start Size: 0.1 ~ 0.2 -> 0.2 ~ 0.6
  • Trails: disabled -> enabled
    • Lifetime: 0.1
    • Minimum Vertex Distance: 0.01
    • Width over Trail: 0.1
  • Renderer
    • Material: 새로 생성한 GrenadeExplosion material 지정
    • Trail Material: 새로 생성한 GrenadeExplosion material 지정

Trail 도 ShootAction 에서 썼던 것을 복사해서 사용.

포물선을 그리며 날아가는 것에는 AnimationCurve 라는 unity 클래스를 활용.

Vector3 moveDir = (targetPosition - positionXZ).normalized;
float moveSpeed = 15f;
positionXZ += moveSpeed * Time.deltaTime * moveDir;

float distance = Vector3.Distance(positionXZ, targetPosition);
float distanceNormalized = 1 - distance / totalDistance;

float maxHeight = totalDistance / 4f;
float positionY = arcYAnimationCurve.Evaluate(distanceNormalized) * maxHeight;
transform.position = new Vector3(positionXZ.x, positionY, positionXZ.z);

73. Destructible Crate (07:56)

Create (=상자) 라는 별도의 개임 객체를 만들고, 해당 객체의 layer 는 Obstacles 로 지정.

GrenateAction 에서 데미지를 주는 로직에서 범위 내에 있는 Crate 에도 데미지를 주도록 수정. Crate 는 HP 개념 없이 한 대만 맞으면 부서지도록 (삭제하도록) 구현.

단 이 과정에서 Crate 가 부서질 경우 Pathfinding 도 업데이트 해줘야 하는데, PathfindingCrate 간 상호 의존성을 만들지 않기 위해 PathfindingUpdater 라는 클래스를 따로 만들고, 해당 클래스에서 DestructibleCrate.OnAnyDestroyed 리스너를 등록하게끔 해서 구현.

강의 67,68,69,70강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

67. Level Design (07:05~, 15:41)

남은 시간에도 열심히 맵을 만들었다. Level Design 이라는 게 말 그대로 맵을 만드는 것을 뜻하는 듯.

68. Unit Move with Pathfinding (17:31)

Unit 의 움직임에 Pathfinding 을 적용한다.

이동 전 이동 가능한 지점을 찾는 데 obstacle 도 고려하기 위해 Pathfinding 과 Raycast 를 추가로 적용한다.

총을 쏠 때도 총을 쏠 수 있는 대상인지 판단할 때 Raycast 를 추가로 적용한다.

69. Intro - Polish (01:24)

새로운 섹션 Polish 에 대한 소개. 공격할 때 화면이 흔들리는 효과, 수류탄/검 공격, 파괴/인터렉션 가능한 오브젝트 등등 게임의 디테일을 구현할 것이라고 한다. 흥미진진.

이 섹션 이후에도 두 개의 보너스 섹션 (Hex Grid System, Multi-Floors) 이 존재하는데, 보너스 섹션을 제외하면 공식적인 마지막 섹션이다. 이 섹션이 끝난 뒤에는 전체적으로 코드 리뷰도 한 번 하고, 본격적인 게임 구현 시작을 준비해야겠다.

70. Screen Shake (09:14)

CinemachineVirtualCamera, ActionVirtualCamera 에 Add Extension 메뉴를 통해 Impulse Listener 를 추가.

  • Reaction Settings
    • Secondary Noise: 6D Shake
    • Amplitude Gain: 10
    • Frequency Gain: 5
    • Duration: 0.5

새로운 게임 객체 ScreenShake 추가. 해당 객체에 Cinemachine Impluse Source 컴포넌트를 추가.

  • Impulse Shape: Rumble, s: 0.1
  • Default Velocity: y: 0.3

ScreenShake 스크립트에는 Shake 메서드를 구현해 카메라를 흔들 수 있는 기능을 제공하게 하고, ShootAction 에는 OnAnyShoot 이벤트를 추가한 뒤, ScreenShakeAction 에서 ShootAction.OnAnyShoot 이벤트를 리스닝 한 뒤에, 리스너에서 ScreenShake.Shake 를 실행하게끔 구현.

정말 이 강의는 스크립트를 추가하고 구현을 나누고 결합을 느슨하게 하는 이런 일련의 행위들에 거리낌이 없다. 예전에 들었던 강의라면 이런 건 최소화하고 말 그대로 동작하게 하는 것에만 집중했을 것 같은데.

강의 66,67강 수강 중

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

66. Pathfinding Obstacles (12:45)

장애물을 위한 Layer 를 새로 만들고, Wall prefab 을 만들어 해당 layer 를 적용한 뒤에, 맵 위에 wall 을 새워놓으면 raycast 로 wall 이 서 있는 칸을 확인해 해당 칸은 pathfinding 에서 사용하지 않도록 수정.

이를 위해 PathNodeisWalable 이라는 필드가 추가되었다.

그런데 다 잘 따라한 것 같은데 장애물을 인식을 못해서 한참 찾다가 raycast 랑 충돌할 layer mask 를 제대로 지정 안한 걸 겨우 찾아냈다.

67. Level Design (~07:05, 15:41)

맵을 만든다. 7분까지도 조용히 몇 마디 말 없이 맵만 만든다.

강의 65강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

65. Pathfinding Implementation (30:18)

이 수업의 단일 강의 중 제일 길었던 것 같은데, pathfinding 알고리즘을 구현해서 0,0 으로부터 마우스가 있는 지점까지 길을 찾은 다음 디버그 라인을 그리는 것까지 구현하는 내용이었다. 아직 장애물이 아직 없기 때문에 결과가 특별하진 않다.

다음 수업에서는 장애물을 고려해서 개선한다고 한다.

예전에 잠깐 공부했던 알고리즘 PS 가 이렇게 도움이 될 줄이야. A* 알고리즘이 다익스트라랑 매우 비슷하기도 해서, 이해가 쉬웠다.

강의 64강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

64. Pathfinding Script (12:17)

Pathfinding 을 본격적으로 구현하기에 앞서 해당 구현을 가시적으로 볼 수 있는 디버그용 객체를 생성.

  • Pathfinding 클래스 및 개임객체 추가
  • Pathfinding 에서 사용할 그리드 객체인 PathNode 추가
  • 기존에 있던 GridDebugObject 를 활용해 PathfindingGridDebugObject 를 추가
  • LevelGrid 에서 기존에 그리던 GridDebugObject 를 그리지 않도록 임시 처리하고
  • Pathfinding 에서 PathfindingGridDebugObject 를 그리게 함

본격적인 Pathfinding 구현은 다음 강의부터 할듯.

강의 61,62,63강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

61. C# Generics (08:47)

그냥 generic 개념을 가르쳐준다. 특별할 건 없다.

62. Grid System Generics (06:27)

generic 을 GridSystem 에 적용했는데, 본격적으로 적용했다기 보다는 다음 강의에 앞서 사전 작업을 해두었다는 느낌.

63. Pathfinding Overview (04:47)

  • G = Walking Cost from the Start Node
  • H = Heuristic Cost to reach End Node
  • F = G + H

경로 찾기 알고리즘에 대해 설명하는데, A Star Algorithm 이라고 한다. 찾아보니 다익스트라를 확장한 알고리즘이라고 한다.

강의 58,59,60강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

58. Enemy AI Basic (17:11)

UnitManager 스크립트를 추가해서 해당 스크립트에서 모든 Unit 을 갖고 있도록 구현.

  • 이렇게 되면 UnitManager 는 모든 Unit 의 생성보다 빠르게 초기화되어 Unit 의 생성 이벤트를 들을 수 있어야 하므로, Edit - Project Settings - Script Execution Order 에서 순서 조정이 필요하다.

screenshot

이후 예전에 만들어 두었던, 턴이 돌아오면 일정 시간 이후 NextTurn 만 하게 작성해두었던 EnemyAI 스크립트를 수정한다.

  • 턴이 왔을 때 UnitManager 로부터 모든 적 Unit 목록을 가져온다.
  • Unit 목록을 순회하면서 액션이 가능한 Unit 들의 액션을 실행한다.
  • 모든 액션이 끝나면 NextTurn 으로 넘긴다.

이번 강의에서는 적 UnitSpinAction 만 하게 구현했는데, 다음 강의에서 개선할 것으로 보인다.

59. Enemy AI Complex (18:22)

각 액션에 점수를 계산할 수 있는 로직을 넣고, 적 Unit 액션 실행 전 실행 가능한 액션들의 점수를 비교해 점수가 가장 높은 액션을 실행하게끔 구현.

점수를 관리하는 클래스 (지금 시점에서는 struct 로 작성해도 문제 없어보이는 클래스) EnemyAIAction 클래스를 만들어서 사용한다. 해당 클래스에는 gridPosition 과 점수 필드가 존재하는데, 이는 같은 액션이라 할지라도 어느 지점으로의 액션인지에 따라 점수가 다를 수 있기 때문.

  • 예를 들어 moveAction 의 경우 적에게 가까이 가는 것이 멀리 가는 것 보다 점수가 높을 것이고, shootAction 의 경우 HP 가 적은 적을 노리는 게 점수가 높을 수 있다.

액션 관련한 비효율적인 코드가 몇몇 눈에 띄는데, 후속 강의에서 개선할 것으로 보인다.

60. Intro - Pathfinding (01:01)

Pathfinding 섹션에 대한 소개.

맵에 지나다닐 수 없는 지형지물 (예, 벽) 을 만들고, 그런 맵 위에서 적에게 접근할 수 있는 최적의 경로를 찾는 알고리즘을 적용한다고 한다.

맵을 구현하기 위해 GridSystem 에 대한 리팩토링이 필요하고, 이를 위한 c# generic 에 대해서도 간단히 알려준다고 한다.

강의 55,56,57강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

55. Unit World UI (15:55)

Unit Prefab 내부에 UI Canvas 생성. Render Mode 는 World Space 로.

UI 만 만들어 붙이면 캐릭터가 회전할 때 같이 회전해서 보기 불편하므로 항상 카메라를 바라보도록 하는 스크립트 LookAtCamera 도 작성해서 붙여준다.

LateUpdate 라이프 사이클도 사용. 이건 업데이트가 끝난 직후에 실행된다.

UI Image 객체를 사용할 때, 단순 색상 적용이 아닌 이미지 리소스를 적용해야만 보이는 옵션이 있다. 이 경우 Image - Source Image 에서 동그라미를 클릭해 Unity 에서 기본으로 제공하는 리소스들을 적용해 사용하면 간편하다. 예를 들어 white1px 를 적용하면 색상 적용도 잘 되고, 이미지 리소스 적용 시에만 적용할 수 있는 옵션도 적용할 수 있다. 이 옵션 적용으로 이미지를 체력바처럼 적용할 수 있다.

screenshot

56. Action Camera (14:54)

기존 카메라인 CinemachineVirtualCamera 를 복사해서 ActionVirtualCamera 를 만들고, ActionVirtualCamera 의 Priority 를 올리고 필요할 때마다 활성화/비활성화 해서 사용.

Priority 높은 카메라가 활성화되는 순간 자동으로 해당 카메라로 transition 발생. 이 transition 의 메서드 및 속도는 MainCamera 의 CinemachineBrain 에서 설정.

ActionVirtualCamera 카메라를 총 쏘는 Unit 의 어깨 오른쪽 위에 두고 타겟을 바라보게끔 하는 계산

switch(sender)
{
    case ShootAction shootAction:
        Unit shooterUnit = shootAction.GetUnit();
        Unit targetUnit = shootAction.GetTargetUnit();
        Vector3 cameraCharacterHeight = Vector3.up * 1.7f; // 실제 `Unit` 의 (지면으로부터의) 어깨 부근 좌표

        Vector3 shootDir = (
            targetUnit.GetWorldPosition() - shooterUnit.GetWorldPosition()
        ).normalized;
        float shoulderOffsetAmount = 0.5f; // 어깨에서 오른쪽? 으로 얼마나 띄울 것인가
        Vector3 shoulderOffset =
            Quaternion.Euler(0, 90, 0) * // Y 축 기준 회전
            shootDir *
            shoulderOffsetAmount;

        Vector3 actionCameraPosition =
            shooterUnit.GetWorldPosition() +
            cameraCharacterHeight +
            shoulderOffset +
            shootDir * -1;
        actionCameraGameObject.transform.position = actionCameraPosition;
        actionCameraGameObject.transform.LookAt(
            targetUnit.GetWorldPosition() + cameraCharacterHeight
        );
        ShowActionCamera();
        break;
}

57. Grid Visual Colors (17:50)

액션에 따라 사정거리를 표시하는 GridSystemVisual 의 색상을 변경.

enum 을 만들어서 enum 별로 Material 을 지정하고 사용할 수 있게 한다.

[Serializable]
public struct GridVisualTypeMaterial
{
    public GridVisualType gridVisualType;
    public Material material;
}

public enum GridVisualType
{
    White,
    Blue,
    Red,
    Yellow,
}

이외에도 실제 액션이 가능한 GridPosition 뿐만 아니라 사정거리를 표시하는 것도 구현했고, 비효율적인 렌더링 로직도 개선했다.

강의 53,54강 수강

#1

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

53. Health System (07:52)

HealthSystem 스크립트를 만들어 Unit Prefab 에 부착. 해당 스크립트에서는 체력 수치를 관리하고 public Damage() 메서드를 갖고 있고, 체력이 0이 되었을 때 실행되는 OnDead 이벤트를 제공한다.

54. Ragdoll (15:41)

Unit Prefab 을 복사해서 UnitRagdoll Prefab 생성. 부착되어있던 모든 컴포넌트 삭제. 내부에 있는 Animator 컴포넌트도 삭제하고, 총기 객체는 비활성화해서 안 보이게 처리.

상단 메뉴 Game Object - 3D Object - Ragdoll 선택

screenshot

알맞는 객체를 연결한 뒤에 총기 객체는 다시 활성화.

  • 비활성화해두지 않으면 Ragdoll Wizard 가 총기까지 몸으로 인식해서 Ragdoll 정보 생성.

Ragdoll Prefab 으로 객체를 만들어서 씬에 두면 바로 애니메이션이 실행된다.

충돌 애니메이션이 이상하면 Collider 를 수정해보자. 강의에서는 이상하게 작게 설정되어있던 Collider 하나를 크게 만들어줘서 해결했다.

Ragdoll 설정이 끝났으면, Unit 이 죽을 때 Ragdoll 을 생성해서 보여주자.

public class UnitRagdollSpawner : MonoBehaviour
{
    // ...

    private void Awake()
    {
        // ...
        healthSystem.OnDead += HealthSystem_OnDead;
    }

    private void HealthSystem_OnDead(object sender, EventArgs e)
    {
        Transform ragdollTransform = Instantiate(ragdollPrefab, transform.position, transform.rotation);
        UnitRagdoll unitRagdoll = ragdollTransform.GetComponent<UnitRagdoll>();
        unitRagdoll.Setup(originalRootBorn);
    }
}

그냥 생성만 해버리면 기존 Unit 이 자세와 달리 T 포즈로 나와서 쓰러질 것이므로, 기존 Unit 의 자세와 일치시키자.

public class UnitRagdoll : MonoBehaviour
{
    [SerializeField] private Transform ragdollRootBorn;

    public void Setup(Transform originalRootBorn)
    {
        MatchAllChildTransform(originalRootBorn, ragdollRootBorn);
    }

    private void MatchAllChildTransform(Transform root, Transform clone)
    {
        foreach(Transform child in root)
        {
            Transform cloneChild = clone.Find(child.name);
            if(cloneChild != null)
            {
                cloneChild.position = child.position;
                cloneChild.rotation = child.rotation;
                MatchAllChildTransform(child, cloneChild);
            }
        }
    }
}

#2

오오 래그돌 재밌다.

강의 52강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

52. Shooting Bullet Visual (20:51)

Effect - Trail 객체를 생성해서 총 쏜 효과를 만든다.

  • Trail Renderer
    • Width: 0.05
    • Time: 0.1
    • Min Vertex Distance: 0.01
    • autodestruct: enable
    • Material: Material 객체를 아래 속성으로 새로 만든다.
      • Surface Input
      • Base Map: 적당한 색 (강의에서는 노란색)
      • Emission: enable
        • Emission Map: 적당한 색 (강의에서는 빨간색)

만든 Trail 객체를 Prefab 으로 만든다.

Unit Prefab 내부의 총 객체 안에 ShootPoint 객체를 만들어서 총구에 위치시킨다.

이벤트 리스너에서 원하는 데이터 형식 받기

public event EventHandler<OnShootEventArgs> OnShoot;

public class OnShootEventArgs : EventArgs
{
    public Unit targetUnit;
    public Unit shootingUnit;
}

private void Shoot()
{
    OnShoot?.Invoke(this, new OnShootEventArgs {
        targetUnit = targetUnit,
        shootingUnit = unit,
    });
    targetUnit.Damage();
}

ShootAction 에서 총을 쏘면 이벤트를 듣고 있던 UnitAnimator 에서 총알 발사 애니메이션 실행.

  • 위에서 만든 Trail Prefab (BulletProjectile) 으로 객체를 생성해서 움직인다.
private void ShootAction_OnShoot(object sender, ShootAction.OnShootEventArgs e)
{
    animator.SetTrigger("Shoot");

    Transform bulletProjectileTransform =
        Instantiate(bulletProjectilePrefab, shootPointTransform.position, Quaternion.identity);

    BulletProjectile bulletProjectile = bulletProjectileTransform.GetComponent<BulletProjectile>();

    Vector3 targetUnitShootAtPosition = e.targetUnit.GetWorldPosition();
    targetUnitShootAtPosition.y = shootPointTransform.position.y;
    bulletProjectile.Setup(targetUnitShootAtPosition);
}

public class BulletProjectile : MonoBehaviour
{
    private void Update()
    {
        Vector3 moveDir = (targetPosition - transform.position).normalized;

        float deistanceBeforeMoving = Vector3.Distance(transform.position, targetPosition);
        float moveSpeed = 200f;
        transform.position += moveSpeed * Time.deltaTime * moveDir;

        float distanceAfterMoving = Vector3.Distance(transform.position, targetPosition);

        if(deistanceBeforeMoving < distanceAfterMoving)
        {
            trailRenderer.transform.parent = null;
            Destroy(gameObject);
        }
    }
}

총 쏜 이펙트가 좀 더 자연스럽게 사라지게 하기 위해 BulletProjectile 객체를 없애기 전에 trailRenderer 의 부모를 null 로 하고, autodestruct 옵션을 켜면 trail 애니메이션이 끝날 때 자연스럽게 사라진다.

총 맞은 이펙트를 만들기 위해 Effect - Particle 객체를 만드는 데 옵션을 꽤 많이 설정한다. 일일이 기록하진 않겠다. 그냥 기억 안나면 강의를 다시 보자.

Trail, Particle 과 Material 을 직접 만들어서 적용했는데 퀄리티가 괜찮다. 다음 강의가 Health System 이니 점점 게임 같아지는 것 같아서 재밌다.

강의 50,51강 수강

#1

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

50. Shoot Action (22:05)

ShootAction 을 추가하고 기본 기능을 구현.

여기서 기본 기능이라 함은 GetValidActionGridPositionList, TakeAction, Update 메서드를 구현해서 다른 액션 클래스들과 같이 동작하게 하는 것인데, ShootAction 에서는 state 개념까지 추가했다. 아마 다음 강의에서 이 state 를 사용해 총을 쏘는 애니메이션을 결합하지 않을까 싶다.

51. Unit Animator (08:16)

애니메이션을 전담할 UnitAnimator 클래스 생성

걷기 애니메이션 때는 Animaotr 에서 파라미터로 IsWalking Bool 을 추가했지만, 이번에는 Shoot Trigger 를 추가. Trigger 는 transition 을 한 번 실행하고 종료시키는 일회성 값. (Bool 보다 더 간단한 값이라고 이해하면 될듯)

"Firing Rifle" 로 가는 transition 은 hasExitTime 을 껐지만 "Firing Rifle" 에서 "Idle" 로 나오는 transition 은 hasExitTime 을 켜둔다. 총은 쏘면 자동으로 애니메이션이 끝나는 것이 자연스러우니까.

총알이 발사되지는 않고 총을 쏘는 듯한 액션만 취하는데, 총알은 다음 강의에 추가할 듯.

#2

  • 언제 한 번 날 잡고 전체적으로 코드 리뷰도 해야 할 것 같다.
  • 이제 피곤할 때 강의를 들으면 집중이 잘 안 될 때가 있다. (어제도 사실 강의를 틀긴 틀었는데 도저히 집중이 안 되어서 중간에 그냥 껐다.) 강의 난이도나 코드의 복잡도가 좀 올라간 느낌.

강의 47,48,49강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

47. Turn System (15:57)

TurnSystem, TurnSystemUI 를 추가해서 턴 종료 및 초기화 기능 구현.

이 과정에서 이벤트 리스너 실행 순서 이슈가 생길 수 있는 코드가 있어서, 해당 이슈 해결도 포함.

첫 번째 해결 방법은 Edit - Project Settings - Script Execution Order 메뉴를 사용해 스크립트의 초기화 순서를 강제하고, 그에 따른 이벤트 리스너 부착 순서를 강제해서 실행 순서가 정해지게 하는 방법.

당연하게도 이 방법은 사이드 이펙트를 일으킬 가능성도 높고 여러 리스너가 복잡하게 얽혀 있을 경우에는 근본적인 해결책이 될 수 업다.

다른 해결 방법은 이벤트 리스너를 더 추가해서 해결하는 방법인데, 턴이 종료되었을 때 뿐만 아니라 유닛의 액션 포인트가 바뀌었을 때에 대해서도 이벤트 리스너를 추가했다.

TurnSystem.Instance.OnTurnChanged += TurnSystem_OnTurnChanged;
Unit.OnAnyActionPointsChanged += Unit_OnAnyActionPointsChanged;

48. Intro - Enemies and Combat (00:59)

Enemies & Combat 섹션 소개.

49. Enemy (12:58)

기존 Unit Prefab 으로 Create - Prefab Variant 메뉴를 선택해 새로운 Prefab UnitEnemy 생성.

  • 그리고 UnitUnitEnemy 가 공통으로 사용하는 클래스 UnitisEnemy 플래그 추가.
  • TurnSystem 에는 isPlayerTurn 플래그 추가.

End Turn 버튼 누르면 enemy 에게 턴이 넘어가고, 적 턴에는 특정 시간이 지난 후 턴을 넘기도록 구현. 적 AI 는 이후 강의에서 자세히 구현할 듯.

강의 43,44,45,46강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

43. Generic Take Action (11:23)

예상대로 액션 메서드와 유효성 검사 메서드를 일반화

public abstract class BaseAction : MonoBehaviour
{
    public abstract void TakeAction(GridPosition gridPosition, Action onActionComplete);

    public virtual bool IsValidActionGridPosition(GridPosition gridPosition)
    {
        List<GridPosition> validGridPositionList = GetValidActionGridPositionList();
        return validGridPositionList.Contains(gridPosition);
    }

    public abstract List<GridPosition> GetValidActionGridPositionList();
}

액션 실행 코드도 매우 간결해짐

if(selectedAction.IsValidActionGridPosition(mouseGridPosition))
{
    SetBusy();
    selectedAction.TakeAction(mouseGridPosition, ClearBusy);
}

좀 더 개선할 수 있을 것 같긴 한데, 강의에서도 추후 계선할 것이라고 하니 기다려보기로.

44. Selected Action UI (07:57)

현재 선택된 액션을 UnitActionSystem 에 저장해두고, 액션이 바뀔 때마다 UnitActionSystemUI 의 모든 버튼을 순회하면서 저장해둔 액션과 동일한 액션을 들고 있는 버튼에 활성화 표시를 해준다.

45. Action Busy UI (06:46)

액션 중에는 UnitActionSystemUI 를 사용하지 못하도록, 새로운 UI 인 ActionBusyUI 를 만들어서 해당 UI 로 UnitActionSystemUI 를 가려버리는 기능을 구현. 이 강의에서 드디어 파라미터를 전달하는 이벤트 핸들러 코드가 등장.

46. Action Points (13:20)

액션마다 포인트를 부여하고 액션을 사용하면 포인트를 소모하는 로직 구현

각 액션 클래스에서는 자신이 몇 포인트가 필요한 액션인지 알려줄 메서드 작성

public virtual int GetActionPointCost()
{
    return 1;
}

Unit 은 자신의 액션 포인트를 소유하고 있고, 특정 액션에 대해 해당 액션을 실행할 수 있는 액션 포인트가 있는지 확인하고 소모하는 메서드들을 작성

public bool TrySpendActionPointsToTakeAction(BaseAction baseAction)
{
    if(CanSpendActionPointsToTakeAction(baseAction))
    {
        SpendActionPoints(baseAction.GetActionPointCost());
        return true;
    } else
    {
        return false;
    }
}

public bool CanSpendActionPointsToTakeAction(BaseAction baseAction)
{
    return actionPoints >= baseAction.GetActionPointCost();
}

private void SpendActionPoints(int amount)
{
    actionPoints -= amount;
}

UnitActionSystem 에서는 액션을 실행하기 전 액션 포인트를 확인하고 소모하는 로직을 추가. 그리고 UI 를 업데이트하는 로직도 추가

if(!selectedUnit.TrySpendActionPointsToTakeAction(selectedAction))
{
    return;
}

SetBusy();
selectedAction.TakeAction(mouseGridPosition, ClearBusy);

OnActionStarted?.Invoke(this, EventArgs.Empty);

코드 마지막줄에 나온 OnActionStarted 콜백을 통해 UnitActionSystemUI 가 업데이트 됨

강의 42강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

42. Click UI to Select Action (09:45)

마지막에 누른 버튼 기준으로 어떤 액션을 할 지 결정.

switch (selectedAction)
{
    case MoveAction moveAction:
        moveAction.Move();
        break;
    case SpinAction spinAction:
        spinAction.Spin()
        break;
}

다음 강의에서 위 코드를 개선한다고 하는데 아마 Move(), Spin() 메서드를 일반화하지 않을까 싶다.

강의 40,41강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

40. UI Setup (02:36)

씬에 UI - Canvas 생성

  • Canvas Scaler
    • UI Scale Mode 에서 "Scale With Screen Size" 선택
    • Reference Resolution 은 1280x720 ㅡ Match 는 Height 1

Canvas 안에 UI - Image 생성

  • Rect Transform - 기준 위치를 Top Left 로 설정.
  • Pos X, Pos Y 도 적당한 수치 넣기

Image 는 테스트로 생성해봤던 것인지 다음 강의에서는 삭제되어있다.

41. Unit Action System UI (13:37)

Canvas 안에 UnitActionSystemUI 게임 객체 생성

  • Stretch 로 배치, 위치 크기 모두 0으로 설정 UnitActionSystemUI 안에 ActionButtonContainer 생성
  • Bottom Center 로 배치, 크기는 0으로, 위치는 하단으로
  • Grid Layout Group 컴포넌트를 붙여서 자식들 위치 지정 그 안에서 UI 버튼 prefab 을 만든 다음에 여러 개 배치
  • Outline, Shadow 컴포넌트 붙이고 폰트 스타일 바꾸면서 스타일 조정
  • 위치는 Left Top 인데, 부모가 Grid Layout Group 이라서 별 의미 없을 듯

이후 UnitActionSystemUI 에서 각 유닛이 선택될 때마다 가능한 액션 목록을 가져와 버튼을 동적으로 만드는 코드를 작성함.

눈여겨볼만한 코드는 공통 부모 클래스인 BaseAction 클래스를 사용해 액션 클래스들을 한 번에 다 가져와 사용하는 것과

baseActionArray = GetComponents<BaseAction>();

UnitActionSystemUI 에서 UnitActionSystemOnSelectedUnitChanged 를 활용하는 것과

private void Start()
{
    UnitActionSystem.Instance.OnSelectedUnitChanged += UnitActionSystem_OnSelectedUnitChanged;
    CreateUnitActionButtons();
}
private void UnitActionSystem_OnSelectedUnitChanged(object sender, EventArgs e)
{
    CreateUnitActionButtons();
}

BaseAction 에 적절한 abstract 함수를 만들어 사용하는 것

public abstract string GetActionName();

특히 마지막 코드는 앞으로도 클래스 상속을 제대로 활용할 것 같아서 기대가 크다.

강의 38,39강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

38. Base Action Class (13:37)

테스트를 위해 SpinAction 을 추가, 유닛이 회전할 수 있는 함수 추가. MoveAction 도 움직이는 방향을 향해 유닛을 회전시키므로 두 액션이 충돌. 따라서 우리는 (어차피 턴베이스 게임이므로) 한번에 한 액션만 할 수 있게 해서 이 충돌을 막는다.

충돌을 막기 전에 코드를 정리한다. BaseAction 이라는 추상 부모 클래스를 만들어서 이 클래스에서 공통 로직을 관리하고 SpinActionMoveAction 이 이 로직을 상속받게 한다.

아직 BaseAction 에 별 코드가 없기 한데, 이후 강의에서 점점 더 많아질 듯.

39. Single Active Action (11:50)

동시에 두 액션이 실행되는 걸 막기 위해 UnitActionSystem 에 플래그를 세우고, 각 액션에는 액션 성공 delegate (= 콜백) 를 전달해서 액션 시작/종료 시에 플래그 값을 변경하도록 한다.

delegate 를 직접 선언할 수도 있지만

    public delegate void SpinCompleteDelegate();
    private SpinCompleteDelegate onSpinComplete;

c# 에서 제공하는 delegate 클래스를 가져다 쓸 수도 있다.

using System;

    private Action onSpinComplete;

onSpinCompleteonActionComplete 라고 이름 바꿔서 BaseAction 클래스에 일반화.

강의 35,36,37강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

35. Move Action (05:01~, 12:27)

어제 해결 못한 에러를 간신히 해결. GridDebugObject 가 prefab 이외에도 객체로 씬에 존재하고 있어서 생긴 문제였는데, 정확한 이유는 모르겠다. 여튼 해당 객체를 삭제해서 에러 해결.

이동과 관련된 구현을 별도의 스크립트로 나누고, 해당 스크립트에서 이동 가능한 GridPosition 을 값 검증 없이 디버깅 로그로 찍어보는 것까지 진행.

값 검증을 해서 유효한 GridPosition 을 뽑아서 실제로 GridPosition 으로 이동시키는 건 다음 강의에서 할 듯.

36. Move Action Validate (10:49)

유효한 GridPosition 을 찾아내는 코드를 작성. 유효한 GridPosition 이라 함은

  1. GridSystem 안에 위치하는지
  2. 지정된 범위 안에 위치하는지
  3. 현재 해당 Unit 이 머무르는 위치는 아닌 것인지
  4. 다른 Unit 이 머무르는 위치는 아닌 것인지.

를 체크하는 것.

LevelGrid, GridSystem, GridObject, GridPosition, Unit, UnitActionSystem, MoveAction 각 클래스의 역할과, 클래스를 분리하고 메서드를 만드는 컨벤션에 익숙해질 필요가 있어보인다.

37. Grid Visual (15:05)

Unit 의 이동 가능한 GridPosition 을 강조하는 UI 구현. 필요한 GridPosition 마다 동적으로 생성하는 게 가장 효율적이지만, 해당 구현은 다소 복잡하기 때문에 (성능 개선은 나중에 하기로 하고) 당장은 쉬운 방법으로 구현.

  • GridSystemVisualSingle 이라는 빈 객체 생성.
    • 해당 객체에 Quad 객체 생성
      • Mesh COllider 컴포넌트는 삭제
    • 텍스쳐를 강의 페이지에서 다운받아서 프로젝트에 추가하고
    • Material 을 새로 만들어서
      • 해당 Material 의 Subface Inputs - Base Map 으로 텍스쳐 지정
      • Surface Options - Surface Type 을 Transparent 로
    • 해당 Material 을 아까 만든 Quad 의 Mesh Renderer - Materials - Element 0 에 지정
    • 모양(회전,크기)은 Quad 객체에서 관리. 위치는 GridSystemVisualSingle 에서 관리
    • 해당 객체를 Prefabs 폴더에 드래그앤드랍 해서 Prefab 으로 만들자.

GridSystemVisual 이 현재 보여줘야할 grid UI 를 선택하고, 각 GridSystemVisualSingleShow/Hide 한다.

이번 강의에서는 단순히 UnitActionSystemselectedUnit 기준으로 움직일 수 있는 칸을 표시하게끔 했지만 이후 강의에서는 좀 더 많은 조건에 대응 가능하도록 한다.

강의 34,35강 수강 중

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

34. Intro - Actions and UI (00:48)

"Grid System & Camera" 섹션이 끝나고 "Actions and UI" 섹션으로 진입. 이 섹션에서는 유닛들의 액션과 UI 를 구현하는 것에 대해 배운다.

35. Move Action (~05:01, 12:27)

이동 관련 코드를 별도의 스크립트 MoveAction 으로 분리한 뒤 실행했는데, 갑자기 런타임 에러가 발생. 다행히 이전 강의까지 듣고 커밋을 해놓은 상태여서 stash 해보았는데도 여전히 런타임 에러 발생. 로그를 찍으며 원인을 찾아보고 있는데 쉽지 않다..

강의 31,32,33강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

31. Cinemachine (06:18)

카메라 컨트롤을 위해 Cinemachine 패키지를 프로젝트에 설치

Cinemachine 의 Virtual Camera 게임 오브젝트를 프로젝트에 추가하면 Main Camera 객체와 연결된다. 이제 카메라 설정 및 조작은 Cinemachine 의 Virtual Camera 를 통해 한다.

Cinemachine Virtual Camera 설정

  • Follow: 카메라가 따라다닐 객체
  • Look at: 카메라가 바라볼 객체
  • Lens - Verical FOV: 화면 시야각
  • Body
    • Follow Offset: 따라다닐 객체와의 거리
    • X Damping, Y Damping, Z Dumping: 카메라 위치가 전환되는 속도
  • Aim
    • Horizontal Damping, Vertical Damping: 대상의 축 전환 시 카메라 전환 속도

32. Camera Move and Rotate (06:20)

    private void Update()
    {
        Vector3 inputMoveDir = new Vector3(0, 0, 0);
        if(Input.GetKey(KeyCode.W))
            inputMoveDir.z = +1f;
        if(Input.GetKey(KeyCode.S))
            inputMoveDir.z = -1f;
        if(Input.GetKey(KeyCode.A))
            inputMoveDir.x = -1f;
        if(Input.GetKey(KeyCode.D))
            inputMoveDir.x = +1f;

        float moveSpeed = 10f;
        Vector3 moveVector = transform.forward * inputMoveDir.z
            + transform.right * inputMoveDir.x;
        transform.position += moveSpeed * Time.deltaTime * moveVector;

        Vector3 rotationVector = new Vector3(0, 0, 0);
        if(Input.GetKey(KeyCode.Q))
            rotationVector.y = +1f;
        if(Input.GetKey(KeyCode.E))
            rotationVector.y = -1f;

        float rotationSpeed = 100f;
        transform.eulerAngles += rotationSpeed * Time.deltaTime * rotationVector;
    }

33. Camera Zoom (14:19)

강사는 줌 방식을 카메라의 followOfset 의 y 축을 조정하는 방식으로 구현한다. 이는 개인에 따라 원하는 줌 방식이 아닐 수 있으므로, 원하지 않을 경우 다른 구현 방법을 찾아보자.

Cinemachine 패키지가 followOffset 을 조정할 수 있는 친절한 인터페이스를 제공하지 않으므로, 일련의 과정을 통해 Cimemachine 내부에서 followOffset 을 관리하는 컴포넌트를 찾아낸 다음, 해당 컴포넌트를 통해 해당 값을 조정한다.

    float zoomAmount = 1f;
    if(Input.mouseScrollDelta.y > 0)
    {
        targetFollowOffest.y -= zoomAmount;
    }
    if(Input.mouseScrollDelta.y < 0)
    {
        targetFollowOffest.y += zoomAmount;
    }
    targetFollowOffest.y = Mathf.Clamp(targetFollowOffest.y, MIN_FOLLOW_Y_OFFSET, MAX_FOLLOW_Y_OFFSET);

    float zoomSpeed = 5f;
    cinemachineTransposer.m_FollowOffset = Vector3.Lerp(
        cinemachineTransposer.m_FollowOffset,
        targetFollowOffest,
        Time.deltaTime * zoomSpeed
    );

한 가지 주의할 것은, Lerp 함수의 설명은 "Linearly interpolates between two points." 이지만, 적용한 구현 상 실제로는 linear 하게 적용되지 않고 ease out 방식으로 적용될 것이다.

강의 30강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

30. Level Grid (08:19~, 19:17)

GridSystem 위에서 Unit 이 움직일 때 올바른 위치의 GridPostiion 에 위치시키는 코드를 작성.

그것을 위해 GridPosition struct 에 ==, !=, Equals 등의 operator 들을 overriding 했다.

  • LevelGrid (singletone)
    • GridSystem
      • GridObject (2d array)
        • GridPosition
        • Unit (array)
          • GridPosition

GridSystem 바깥으로 Unit 이 나갈 때 앱이 죽는 건 추후 처리할 예정.

강의 29강 수강, 30강 수강 중

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

29. Grid Object Debug (06:25)

GridObject.ToString() 을 구현, GridObject 의 위치에 GridObject.ToString() 을 렌더링해줄 GridDebugObject 를 구현

30. Level Grid (~08:19, 19:17)

LevelGrid 클래스를 추가. 해당 클래스는 gridSystem 객체를 갖고 있고, unitgridSystem 의 특정 gridObject 에 할당할 수 있는 메서드를 제공함.

일단 오늘은 반절 정도만 들어서 딱 위 정리한 내용만큼 구현했고, 남은 내용은 unit 이 위치 이동을 할 때 알맞은 gridObject 에 자동으로 할당하는 것.

강의 27,28강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

27. Grid System (13:45)

그리드 시스템 관리를 위한 GridSystem 클래스를 새로 생성.

  • 이 클래스는 MonoBehaviour 를 상속받지 않음.
  • MonoBehaviour 를 상속 받지 않기 때문에 생성자를 직접 구현해야 함.
  • 좌표에는 x,y 대신 x,z 를 사용한다. 그렇다면 그리드 시스템의 크기는 width/length 가 맞겠으나 관습적으로 width/height 가 더 가독성 있어서 width/height 를 사용한다.

그리드 상의 위치를 표현하기 위해 GridPosition 이라는 struct 생성

  • struct 는 값, class object 는 레퍼런스. 때문에 struct 는 함수 인자로 전달하면 값이 복사되어 전달되는 것이고, 해당 struct 를 수정해도 원본은 수정되지 않는다.

Debug.DrawLine(): 디버깅을 위해 선을 그릴 수 있다.

28. Grid Object (08:57)

GridSystem 의 각 그리드 칸에 쓰일 클래스 GridObject 생성.

모든 GameObjectTransform 을 가지고 있다. 모든 TransformGameObject 를 가지고 있다. 따라서 둘은 웬만한 상황에서 interchangable 하다. 취향과 상황에 맞게 골라 사용하자.

텍스트 오브젝트 가운데 정렬 편하게 하는 법 = 가로 가운데 정렬 + 세로 가운데 정렬 + 너비 0 + 높이 0 + Wrapping Disabled

이번 강의에서는 클래스 만들어서 GridSystem 안에서 필요한 만큼 객체를 만들어서 멤버로 할당하는 것 까지만.

강의 23,24,25,26강 수강

#1

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

23. Unit Selected Visual, Events (10:44)

선택된 유닛을 강조하기 위한 별도의 3D 오브젝트 SelectedVisual 를 만들어 Unit Prefab 에 부착.

해당 오브젝트에는 SelectedUnitVisual 이라는 이름의 스크립트 부착.

SelectedUnitVisual 스크립트는 현재 Unit 이 선택된 Unit 인지 아닌지에 따라 SelectedUnit 오브젝트를 보이게/안보이게 함.

Observer Pattern 으로 selectedUnit 이 바뀔 때마다 이벤트 발송.

public event EventHandler OnSelectedUnitChanged;
private void SetSelectedUnit(Unit unit)
{
    seleectedUnit = unit;
    OnSelectedUnitChanged?.Invoke(this, EventArgs.Empty);
}
public Unit GetSelectedUnit()
{
    return seleectedUnit;
}

관련해서 설명을 많이 해줬는데 이 강의가 유난히 말이 빠르고 발음이 구분이 잘 안 되어서, 코드로 보이는 것 이상은 잘 이해 못한 것 같다. 그나마 다행인 건 관련해서 이후 강의에서 코드를 개선할 예정이라고 하니, 그 때 다시 설명을 듣고 자세히 이해하는 것으로 하자.

Unit Prefab 내부에 있는 SelectedUnitVisual 스크립트는 Prefab 외부에 있는 (= 씬에 있는) 객체의 레퍼런스를 알 수 없다. 따라서 이벤트를 수신할 수 없는 상황인데, 이건 다음 강의에서 Singletone Pattern 을 사용해서 해결한다.

24. Unit Selected Visual, Singleton (09:10)

강의에 나온 Singletone Pattern 코드는 익히 알려진 것과 크게 다르지 않았음.

Awake()Start() 보다 항상 먼저 실행됨. 따라서 내부 초기화는 Awake() 에서, 외부와 연결된 초기화는 Start() 에서 하면 좋다.

private void Start()
{
    UnitActionSystem.Instance.OnSelectedUnitChanged += UnitActionSystem_OnSelctedUnitChanged;
    UpdateVisual();
}

private void UnitActionSystem_OnSelctedUnitChanged(object sender, EventArgs empty)
{
    UpdateVisual();
}

private void UpdateVisual() {
    Unit selcectedUnit = UnitActionSystem.Instance.GetSelectedUnit();
    if(unit == selcectedUnit)
    {
        meshRenderer.enabled = true;
    } else
    {
        meshRenderer.enabled = false;
    }
}

25. Intro - Grid System and Camera (00:52)

Grid 시스템과 카메라 조작을 구현하는 섹션으로 넘어가봅시다.

26. Grid System Design (03:54)

Grid 시스템 설계 개요.

#2

Unit 를 위한 .gitignore https://github.com/github/gitignore/blob/main/Unity.gitignore

강의 20,21,22강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

20. Unit Animator Parameters (07:17)

Animation Paramters 는 네 종류가 있다. Float, Int, Bool, Trigger.

Trigger 는 boolean 비슷한 특수한 값이다. true 로 값을 바꾼 뒤 false 로 직접 바꿔줄 필요가 없다. 나중에 shoot 애니메이션 (총 쏘는 애니메이션?) 적용 시 사용해 볼 것이다.

Animator - Parameters 메뉴에 파라미터를 추가하고, Transition 의 Inspector - Conditions 섹션에서 해당 파라미터를 조건으로 지정해준다.

screenshot

그 다음 스크립트에서 코드 작성

[SerializeField] private Animator unitAnimator;
unitAnimator.SetBool("IsWalking", true);
unitAnimator.SetBool("IsWalking", false);

21. Unit Rotate when Moving (05:10)

객체를 회전시키는 방법은 세 가지.

  • transform.rotation: Quaternion 사용
  • transform.eulerAngles: 각도 사용
  • transform.forward: Vector 사용

여기서는 가장 간단한 transform.forward 사용

    private void Update()
    {
        // ...
        float rotateSpeed = 10f;
        transform.forward = Vector3.Lerp( // 두 백터를 interpolate 해주는 함수
            transform.forward, // 현재 방향 백터. 매 프레임마다 바뀔 수 있음.
            moveDirection, // 목표 방향 백터.
            Time.deltaTime * rotateSpeed // rotateSpeed 를 곱해주지 않으면 너무 느리다.
        );
        // ...
    }

22. Unit Selection (14:10)

여러 유닛 중 원하는 유닛을 선택하는 기능 구현.

일단 씬에 있던 기존 Unit 객체를 Project 로 드래그앤드랍 해서 재사용 가능하게 만들고, 재사용 가능해진 객체 (강의에서는 Prefab 이라고 말한다.) 를 다시 드래그앤드랍 해서 씬에 Unit 객체를 추가.

특정 객체 선택을 위해서는 마우스 Raycast 기능을 써야 하는데, 이 경우 객체가 충돌 가능해야 한다. 하지만 현재 Unit 객체는 선택이 가능하지 않으므로 Collider 를 설정해줘야 한다.

  • Collider 를 특정 객체에만 지정하지 말고 그 객체의 Prefab 에 모두 적용되게끔 해줘야 한다.
  • Collider 의 IsTrigger 플래그를 켜면 실제 물리적인 계산은 하지 않고 트리거로서만 동작하는 듯 하다.

Unit Prefab 에 Layer 도 "Units" 이라는 이름으로 새로 만들어줘서 지정.

private bool TryHandleUnitSelection()
{
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    if(Physics.Raycast(
        ray,
        out RaycastHit raycastHit,
        float.MaxValue,
        unitLayerMask
    ))
    {
        if(raycastHit.transform.TryGetComponent<Unit>(out Unit unit))
        {
            seleectedUnit = unit;
            return true;
        }
    }

    return false;
}

특정 스크립트의 초기화를 빨리 하고 싶을 때는 Edit - Settings - Script Excution Order 를 사용.

screenshot

apply 누르는 순간 값이 자동으로 조금 바뀌는 데 중요한 건 아닌듯.

강의 19강 수강

#1

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

19. Unit Animator (15:48)

Animator 컴포넌트를 사용해 애니메이션을 객체에 연결하는 방법을 설명한다.

  • Animator 컴포넌트를 캐릭터 객체에 부착
    • Apply Root Motion - 애니메이션을 실행만 할 뿐만 아니라 애니메이션에 내장되어 있는 이동까지 실행
  • Animator 의 Transition: 애니메이션과 애니메이션을 연결
    • Has Exit Time: true 면 현재 애니메이션이 끝날 때 Transition 이 자동으로 실행. false 면 현재 애니메이션이 끝나면 애니메이션이 끝남 (false 면 아래 조건들이 당장은 다 의미 없음. 다음 수업에서 Parameter 를 적용해야 의미가 생김)
    • Settings
      • Exit Time: 현재 애니메이션이 다음 애니메이션으로 넘어가기 시작하는 타이밍. 1이면 현재 애니메이션이 끝나야 넘어가고 0.9 라면 애니메이션이 90% 정도 실행된 시점에 넘어가기 시작한다.
      • Fixed Duration: true/false 에 따라 Transition Duration 값이 seconds/% 로 단위가 바뀜
      • Transition Duration: 0.25% 라면 현재 애니메이션의 25%에 해당하는 시간동안 transition 실행됨. 0.25s 라면 0.25초 동안 transition 실행됨.
      • Transition Offset: transition 이 시작할 때 다음 애니메이션의 시작 지점. 0 으로 해놓으면 transition 이 시작될 때 다음 애니메이션도 시작. 0.5 로 해놓으면 transition 이 시작될 때 다음 애니메이션은 중간부터 시작.
      • Interruption Source: 다른 transition 이 치고 들어올 때 셋팅이라는 데. 복잡하기도 하거니와 당장은 알 필요 없기 때문에 강의에서도 대충 넘어감.
  • 캐릭터가 총을 들게 하려면
    • 총 객체를 먼저 씬에 넣고
    • 캐릭터 객체를 열어서 손을 찾고
      screenshot
    • 손의 자식으로 총 객체를 넣고
    • 해당 총 객체의 position, rotation, scale 을 조정
  • 애니메이션마다 총이 위치/방향 등이 다르게 적용될 수 있으므로, 특정 애니메이션에서 총의 위치/방향 등을 조정하고 싶다면 Animation 에서 keyframe recording mode 를 활성화해서 조정하면 된다. 그렇게 하면 총의 위치/방향이 해당 애니메이션에서만 바뀐다.
    screenshot
    • 애니메이션이 Read Only 일 경우 복사해서 새로운 애니메이션을 만들면 된다.

#2

강사 아저씨가 말이 빨리지거나 발음이 뭉게질 때 좀 힘들다. 자막도 자동 생성 자막이라 그럴 때는 큰 도움이 안 되고. 일단은 큰 맥락을 이해할 수 있다면 모든 문장과 단어를 완벽하게 해석하는 건 적당히 넘어가고 있다.

강의 16,17,18강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

16. Unit Click to Move (03:06)

이전 강의들에서 했던 "객체를 움직이게 하는 것"과 "마우스 클릭 지점을 찾는 것"을 조합해서 마우스 클릭 지점으로 객체를 움직이는 내용.

17. Public Vs SerializeField Private (06:15)

  • public 필드: Unity 에디터와 모든 다른 스크립트에서 접근 가능
  • private 필드: 자기 자신만 접근 가능. Unity 에디터도 다른 모든 스크립트도 접근 불가.
  • [SerializeField] private 필드: 자기 자신과 Unity 에디터에서만 접근 가능. 다른 모든 스크립트는 접근 불가.

18. Unit Animations Setup (05:22)

  • mixamo 에서 Unity 에 적용할 수 있는 무료 애니메이션을 다운받을 수 있다.
  • 받은 애니메이션들의
    • Rip - Animaytion Type 은 Humanoid 로 변경
    • Animation
      • 이름은 mixamo.com 대신 어울리는 이름으로
      • Loop Time 은 체크
      • Root Transform Rotation - Based Upon 을 Original 로 해서 애니메이션 방향 초기화

애니메이션 파일을 다운 받고 해당 파일 초기화만 진행.. 객체에 연결하는 건 다음 강의인가..

강의 14,15강 수강

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

14. Mouse Raycast (07:59)

  • 옛날 Unity 에서는 Camera.main 코드가 항상 카메라를 찾아왔기 때문에 비용이 비쌌지만, 2020년 버전 이후로는 캐싱이 자동으로 되기 때문에 그런 문제가 사라졌다.
  • Raycast 로 카메라와 객체가 충돌하는 지점을 찾을 수 있다.
  • Raycast 로 충돌하는 객체는 Physics Collider 가 지정된 객체 뿐이다. 이것은 객체가 보이는지/안보이는지와는 관계 없다.

15. Mouse World Position (12:04)

  • Raycast 계산한 지점에 구체 객체 따라 다니게 하기. 재밌다.
  • 재밌네 C# 코드, out. 분명 전에도 본 적 있는데 볼 때마다 신기하다.
    Physics.Raycast(ray, out RaycastHit raycastHit
    
  • LayerMask 를 사용해 원하는 레이어만 Raycast 가 충돌하게 할 수 있다.
  • 게임 오브젝트를 비활성화하는 단축키를 알려주는데 맥이랑 다르다. 맥은 alt + shift + a
  • Raycast postiion 을 쉽게 얻을 수 있는 static 멤버 및 함수를 만드는데, 해당 게임 스크립트를 단 하나의 게임 오브젝트에만 연결하기 때문에 별다른 위험성 없이 적용할 수 있는 코드인듯.

VS Code intelliSense 이슈 해결 + 강의 13강 수강

#1

강의에서 C# 스크립트를 수정하기 시작했는데 VS Code 에서 여전히 Unity 코드의 자동 완성 기능이 동작하지 않아서, 답답해서 강의를 멈추고 찾아봤다.

일단 구글에서 "vscode unity autocomplete" 라고 검색해서 돌아다니다보니 보인 영상 Set up Visual Studio Code with Unity and INTELLISENSE WORKING 2023을 따라해봤는데 잘 되지 않았다.

그런데 해당 영상에서 VS Code 의 extension 에러 메시지를 확인해보라는 말이 있어서 확인해보았는데, 거기에 아래와 같은 에러가 있었다.

"Microsoft.VisualStudio.ProjectSystem.Query.Remoting.QueryExecutionService (0.3)" 서비스를 활성화하지 못했습니다.

해당 에러로 검색을 해보니 [BUG] C# Dev Kit installed but not working #1732 글이 나왔고, 해당 글의 코멘트에서 아래와 같은 내용을 찾았다.

To do that, you need to uncheck this setting located under Extensions -> C# -> C# Dev Kit: Dotnet: Use Legacy Dotnet Resolution
Please make sure you reload VS Code window after changing this setting.

바로 해당 설정을 한 뒤 Developer: Reload Window 를 해보니 위 에러는 없어지고 다른 에러가 나왔다. 지금 와서 찾으려니 정확한 에러 메시지를 찾을 수 없는데, "Apple CPU 인데 x64 용 .net SDK 가 깔려있는 것 같다" 는 내용의 에러였다.

그래서 .net 에서 공식으로 제공하는 .net 삭제 스크립트를 사용해 기존에 깔려있던 .net SDK 를 삭제한 뒤, Apple Sillicon CPU 용 .net 9.0 SDK 를 재설치했다.

재설치한 후에는 뭔가 알 수 없는 에러가 떠서 깔끔하게 재부팅을 했고, 재부팅 후에는 Unity 코드들의 자동 완성 및 타입 추론이 잘 되는 것을 확인할 수 있었다.

#2

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

13. Unit Move (08:01)

  • 캐릭터를 특정 지정으로 움직이기 위한 C# 스크립트 작성
  • 현재 씬뷰와 동일하게 카메라 위치 조정: cmd + shift + f (GameObject - Align With View)

중급자용 코스라서 조금 걱정을 했는데, 이 정도 수준도 (짧지만) 별개의 강의로 만들어 줄 정도라면 앞으로도 따라가는 데 무리는 없을 것 같다.

강의 11,12강 수강

#1

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

이제야 "Introduction" 섹션을 끝내고 "Unit Movement & Selection" 섹션에 진입.

11. Intro - Unit Movement and Selection (01:10)

12. Unit Base Setup (08:11)

  • 오브젝트를 바로 글로벌 레벨에 추가하지 말고, 별도의 빈 GameObject 를 만들어 추가하면서 관리하는 것이 더 좋다.
  • Tool Handle Position 은 항상 "Pivot" 으로
  • Materials 에 있는 Grid 와 Testure 들로 Object 의 표면 질감을 변경할 수 있음
  • 그리드 시작 지점이 영상과 다운받은 asset 이 다를 수 있는데, 댓글에 어떻게 shader 를 수정해 맞출 수 있는지 나와있음

    Add two nodes, a Vector 3 node (Under Input, Basic, Vector3) and add node (under Math, Basic).
    In the Vector 3 node set X and Z to 1 and set Y to 0.
    Connect the Vector3 out connector to one of the in connectors on the Add node.
    Remove the connector from the position node on the triplanar
    Attach the out connector from the Position node to the other connector on the Add node.
    The connect the out of the add node to the position connector on the Triplanar node.

#2

씬뷰 "Q" 상태에서 맥 트랙패드

  • 두 손가락 드래그: 확대/축소
  • 세 손가락 드래그: 상하좌우이동
  • Option 키 누르고
    • 두 손가락 드래그: 확대/축소
    • 세 손가락 드래그: 카메라 회전

강의 10강 수강

#1

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

10. Post Processing (12:37)

렌더링되고 있는 씬에 렌더링 효과를 후처리하는 것. 이 강의에서는 몇몇 더미 오브젝트를 추가한 뒤에 여러 효과를 적용해보며 각 효과들이 어떤 변화를 주는지 알아보았다.

아래 옵션들을 추가 및 변경했다.

  • Global Volume
    • Tonemapping
    • Color Adjustments
    • Bloom - Threshold / Intensity
    • Vignette
  • Directional Light
    • Emission
      • Light Appearance - Temperature
      • Intensity
  • Main Camera
    • Rendring - Anti-aliasing
  • URP-HighFidelity
    • Quality - Anti Aliassing (MSAA)
  • URP-HighFidelity-Renderer
    • SSAO

상세한 효과는 직접 조정해보면서 다시 확인해보자.

어차피 나중에 다시 보게 될 값들인 것 같아서 지금 자세히 들여다보지는 않았다.

아 그런데 15분도 안 되는 강의인데 실제로는 다 듣기까지 거의 1시간이 걸렸는데.. 영어라 더 걸리는 것 같다.

#2

씬뷰에서 움직이기가 불편해서 단축키를 찾아봤다. https://docs.unity3d.com/kr/2018.4/Manual/UnityHotkeys.html

강의 7,8,9강 수강

#1

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

7. Accessing Our Projects (04:38)

강의에서 사용된/작성된 코드에 접근하는 방법에 대해 안내. 주로 GitLab 으로 관리하는 듯 하다.

8. Project Setup (07:56)

Unity Hub 에서 3D URP 프로젝트를 새로 생성한 후, 본격적인 개발 강의에 앞서 IDE 설정을 진행.

9. Render Pipeline and Assets (05:43)

  • Unity 의 Rendering Pipeline 종류에 대해 간단한 소개.
  • 프로젝트에서 Rendering Pipeline 관련 기본 설정 확인.
  • 강의에 사용한 Asset 이 유료 Asset 이라서 결제를 했는데, 알고 보니 강의에서 무료로 제공해주고 있었음.
    • 강의의 Resourses 메뉴에서 다운받을 수 있음
    • 다만 다운받은 파일을 (맥에서는) Finder 에서 기본 압축툴로 압축을 풀면 제대로 압축이 안 풀리는 이슈가 있음. 이 경우 Unarchiver 앱을 받아서 그 앱으로 압축을 풀거나 터미널에서 unzip 명령어를 사용해서 압축을 풀어야 함.

#2

영어 강의는 확실히 자막이 있더라도 한국어 강의보다 집중력을 몇 배로 요구하는 것 같다. 자막도 자동 생성된 자막이라 백퍼센트 맞지 않아서 더더욱 그렇다. "더욱 집중하게 해주고 영어 듣기 연습을 시켜준다"고, 긍정적으로 생각할 수는 있겠다.

블로그 배포 및 강의 이어서 듣기

#1

만든 블로그를 GitHub Pages 에 배포해보았다. 그런데 404 가 뜨길래 서브도메인 관련 설정을 잘못했나 하고 찾다가 나중에야 배포 과정에서의 빌드 문제인 것을 확인했다. 결국 빌드 에러까지 다 수정해서 제대로 배포되는 것을 확인했다.

블로그는 일단 글 목록 페이지는 문제 없이 잘 보이는데 글 상세 페이지의 스타일은 제대로 정리되지 않았고, 글 상세 페이지에서는 새로고침 시 글에 첨부한 이미지가 안 보이는 등의 문제가 있다. 하지만 글 목록 페이지에서 글의 모든 내용을 다 보이게 해놨기 때문에 큰 문제는 없다. 당장 블로그 구현에 시간을 많이 투자하고 싶지는 않기 때문에, 여차하면 글 상세 페이지 자체를 없애는 것도 염두에 두고 있다.

#2

강의 수강: Unity Turn-Based Strategy Game: Intermediate C# Coding

"6. Reminder Of C# Fundamentals (15:02)" 강의를 들었다.

해당 강의는 C# 의 기초 문법과 Unity 의 라이프 사이클 함수에 대한 리마인드 를 했다.

들으며 느낀건데, 이 강의는 Unity 경험도 있는 사람을 대상으로 한 중급자 코스다.. 내게 Unity 경험이래봐야 3년 전에 들은 인프런 강의 뿐인데.. 앞으로 잘 들을 수 있을런지 걱정이 된다. 일단은 더 들어보다가 도저히 안 되겠으면 초급자 코스를 찾아가야겠다.

강의 보며 개발 환경 셋팅

Unity Turn-Based Strategy Game: Intermediate C# Coding 의 "4. Set Up Unity & VS Code (10:20)" 영상을 보면서 개발 환경 구성을 시도했다.

해당 영상에서 2021.1 버전을 설치하라고 해서 설치하려고 했는데, Unity 홈페이지에서 받은 설치파일로 설치를 하려고 하면 에러 메시지가 계속 떴다.

screenshot

맥의 "개인정보 보호 및 보안" 설정과 관련된 이슈 같은데, 문제는 회사 노트북이라 해당 설정 수정이 안 된다는 것. 학습용이라 회사에 이야기하면 임시로라도 풀어줄 가능성이 높지만 그 경우 주말을 날려야 한다.

screenshot

"그래도 열기" 버튼을 눌러도 설치로 넘어가지 않는다.

어쩔 수 없이 방법을 선회, Unity Hub 를 설치 후 거기서 Unity 를 설치했다. (예전 학습 때는 Unity Hub 를 사용했던 것이 간신히 생각해냈다.) 그나마도 2021.1 버전은 없어서 2021.3.45f1 버전을 설치했다.

(어이없는 게 설치를 위해 멈춰뒀던 강의 영상을 다시 재생하니까 영상에서도 Unity Hub 를 사용하고 있다..)

설치하고 새 프로젝트 만들기 쭉쭉 따라서 진행. 잘 설치된 것까지 확인했다.

그런데 새 프로젝트에서 intellisense 가 작동하지 않아서 원인을 찾아봤는데 알 수 없었다. 다음 강의 "5. IntelliSense Issues? (01:44)" 가 intelliSense 에 관한 내용이었는데, 아쉽게도 해당 글의 내용으로는 이슈가 해결되지 않았다.

공부 기록용 최소 기능 블로그

공부 기록을 하면서 공부하면 더 재밌을 것 같아서 블로그를 간단히 만들기로 결정.

Gatsby 로 만든 블로그가 이미 있으나, 거기는 주전공에 대한 내용을 넣고 게임 개발은 별도로 기록하는 게 좋을 것 같아서 nextjs 로 새로 만드려고 한다.

https://cmdcolin.github.io/posts/2023-04-08-nextjs-appdir-blog 이 블로그 글을 따라해서 마크다운으로 정적 페이지 만들기가 가능한지 까지 확인.

이제 스타일만 간단히 넣으면 될 것 같다.

턴제 게임 개발 공부를 위한 강의 선택 및 맛보기

willow 형의 추천으로 Unity Turn-Based Strategy Game: Intermediate C# Coding 결제.

해당 강의의 아래 세 영상을 맛보기로 보았다.

  • Introduction 섹션
    • Promo (02:35)
    • Welcome To The Course (06:23)
    • Community & Support (01:40)

실질적으로 개발에 관련된 내용은 아니고 프로젝트를 소개하는 내용이었는데, 영어 강의를 들을 수 있는지 확인하는 차원에서 들었다. 영어 자막도 있기 때문에, 어떻게든 들을 수 있을 것 같다.