새로운 강의의 지급이 되어 해당 강의를 흡수하고, 개인 과제를 해결하여 제출하는 데에 주말 포함 7일이 주어졌다.
또한 새로운 팀 편성을 하며 팀 스페이스를 새로 작성하였다.
스크럼 로그트러블 슈팅 탬플릿오늘 먹은 거 자랑
매일 회의를 간단하게 기록하거나, 세션에 대한 메모를 하는 스크럼 로그
프로젝트 중 버그나 개선할 점 또는 추가하고 싶은 아이디어를 작성하는 데이터베이스
쓸 사람만 쓰는 먹은 것 자랑 TIA
팀에 활기를 불어넣어줄 것을 기대하며 위와 같은 내용들을 작성하였다.
새롭게 주어진 강의는 3D 프로젝트에 관한 내용이었다. 오늘은 개발 환경을 구성하기 시작하는 단계까지만 진행하였다.
저녁에는 메모리 관리에 관한 특강 세션이 있었고, 유익하게 배운 내용이 많았다.
2. 오늘 학습에 대해
Git Commit Message에 대해
실제 여러 개발자들이 직접 개발에 참여하고 있는 프로젝트들을 살펴보았다.
그 안에서도 많은 개발자들이 동시에 진행중인 PR(Pull Request) 목록들을 살펴보았는데, 기능 추가에 대한 내용을 예로 들면 소문자와 축약형의 단어인 feat보다 대문자로 시작하고 단어 전체의 Feature로 작성하는 개발자가 훨씬 많았다. 비율로 보면 1:5정도거나 그 이상 차이가 나는 정도.
더 Standard한 메시지 형식을 사용하고 싶기 때문에, 이를 반영하여 앞으로 위와 같은 형식을 사용하도록 할 것이다.
Unity3D - Skybox 만들기
Material 생성
Project에서 Material로 생성
Material의 Inspector에서 Shader 설정 및 파라미터 세팅
Inspector에서 Shader를 Skybox - Procedural(...etc)로 변경
Lightning 윈도우. 에디터 우측 아래 아이콘 또는 Ctrl+9로도 열 수 있다.
Window - Rendering - lighting 의 Environment 탭에서 Skybox Material에 위에서 만든 Skybox를 사용
특강: C# 메모리 관리
메모리 : 프로그램/어플리케이션이 실행하는 데 필요한 데이터를 저장
메모리는 아래와 같은 영역을 가진다.
Code : 프로그래머가 작성한 코드를 보관
Data : 어플리케이션 전반에 필요한 데이터 저장(static, const, 전역변수)
Heap : 참조 데이터(객체) 저장
Stack : 어플리케이션 실행 순서에 필요한 데이터 보관(로컬 변수, 매개 변수)
일반적인 변수는 Stack에 저장된다.(int, bool, struct, enum ...) 클래스 변수도 Stack에 저장된다. 다만 클래스 변수의 값은 '주소값'을 가진다. 클래스 내부에 초기화된 데이터들은 Heap에 저장된다. 클래스 변수 내부의 주소값은, 이 데이터들이 저장된 Heap의 위치를 가리킨다.
C#은 GC(Garbage Collection)라는 메모리 관리 기법으로 Heap에서 사용되지 않는 데이터들을 할당 해제하여 메모리의 공간을 확보한다.
가비지를 가능한 적게 하는 프로그래밍이 좋은 개발이다.
문자열을 계속 +=로 잇는 것은 해당 연산을 할 때마다 매번 새로운 공간을 할당받아 효율적이지 못하며 '문자열 파편화'라고 한다. 문자열 파편화를 방지하는 방법은 아래와 같다.
string을 한번에 제작(보간문자열, 스트링 포맷, +연산자)
StringBuilder 사용
처음부터 큰 메모리 공간을 할당받아 문자열을 채워나가는 방식
물론 이 공간을 넘어가는 경우에는 더 큰 공간을 다시 할당받는다.
만약 면접에서 구조체와 클래스의 차이를 물어보는 문제가 나온다면
메모리 동작에 대한 잉해를 갖고 있는지를 물어보는 의도이다.
구조체는 Stack에 데이터를 저장하는 '값형', 클래스는 Heap에 데이터를 저장하는 '레퍼런스형'이다.
코드의 구조에 따라 어느 형태를 취하느냐에 따라 데이터 관리를 더 효율적으로 할 수 있다.
2stems로 분리한다는 설정을 주면, 음원을 vocals.wav와 accompaniment.wav로 분리해준다.
엄청 깔끔하게 분리되는 편은 아니지만 나름 들을만 한 정도이다. (포스트 하단에 결과 첨부)
로컬에서 Spleeter로 음원 분리 환경 구현
python 환경에서 작동하며, 조금 검색을 해 보면 Google Colab에서도 사용할 수 있도록 마련된 환경이 보이는데, 어째서인지 런타임이 강제로 종료되는 등 잘 작동하지 않아 로컬 환경에서 구현하였다.
선행하여 해 주어야 할 것이 두 가지 있다.
pip install spleeter를 통한 spleeter 패키지의 설치
FFmpeg의 설치. 공식 FFmpeg 웹사이트에서 운영체제에 맞는 설치 지침을 확인 할 수 있다.
Windows의 경우, FFmpeg의 바이너리(zip) 파일을 다운로드하고, 압축을 해제한 후, ffmpeg.exe가 포함된 디렉토리를 시스템의 환경 변수 Path에 추가한다.
설치가 완료된 후, 터미널이나 커맨드 프롬프트에서 ffmpeg -version 명령어를 실행하여 FFmpeg가 올바르게 설치되었는지 확인할 수 있다.
위 내용이 선행되었다면, 아래와 같은 내용의 Python 파일을 작성하여 실행한다.
여러가지 옵션을 줄 수 있지만, 일단은 아래와 같이 작성하여 잘 작동하는지 확인한다.
from spleeter.separator import Separator
def main():
# 오디오 파일 분리 설정
separator = Separator('spleeter:2stems')
# 분리할 오디오 파일 경로
audio_path = './song.mp3'
# 결과물을 저장할 디렉토리
output_path = 'output/'
# 오디오 파일 분리 실행
separator.separate_to_file(audio_path, output_path)
# 결과 확인 등의 추가 코드
if __name__ == '__main__':
main()
if __name__ == '__main__':의 형식을 사용하지 않는다면 Python의 multiprocessing 모듈을 사용할 때 발생하는 문제를 겪게 된다.
Windows 환경에서 multiprocessing을 사용할 때, 스크립트의 메인 블록이 올바르게 보호되지 않기 때문이라고 한다.
다른 변수가 없다면, main.py와 song.mp3 이외에 아래와 같은 파일구조가 만들어졌을 것이다.
output/음원이름/ 위치에 결과물이 위치한다
음원소스는 다음의 유튜브 동영상을 사용하였다.
グッバイ宣言 / 百鬼あやめ cover
song.mp3와 이로부터 생성된 vocals.wav(보컬), accompaniment.wav(반주)는 아래와 같다.
성능을 보여주기 위해 첨부하는데, 출력된 파일 크기가 각각 20메가를 넘어, 부득이하게 크기를 줄여 첨부하였다.
팀 프로젝트 '탕후루를 부탁해'를 마무리 지었지만, 해상도 관련 대응이 되지 않는다는 내용이 있어 관련한 버그를 고치고, 개인 학습 시간을 가졌다.
저녁에는 event와 Action에 대한 특강이 있어 강의와 질의시간을 가졌다.
2. 오늘 학습에 대해
팀 프로젝트 중 이슈
변동 해상도에 대응되지 않음
고해상도나 저해상도의 기기에서 실행할 경우
게임 중 해상도의 변경이 있을 경우
현재 개발을 계속 760x1280 해상도에 맞춰 해왔기 때문에, 더 고해상도가 된다면 UI배치가 전체적으로 화면의 가운데에 몰리고, 저해상도의 경우는 UI가 화면 밖으로 나가버리는 이슈가 있었다.
이러한 경우, 아래와 같이 Canvas 오브젝트에서 UI Scale Moce를 기본값인 Constant Pixel Size 에서 Scale With Screen Size 로 수정해주면 해결할 수 있었다.
설정을 적용할 경우, 어느 해상도에서도 Canvas가 스케일링되어 나오게 된다.
목표 탕후루
그러나 현재 스크립팅 된 내용으로는, 목표 탕후루를 Canvas의 position 기준으로 생성 위치를 선정했기 때문에, 해상도가 달라지면 목표 탕후루의 이미지도 엉뚱한 곳에 위치하는 문제가 있었다.
목표 탕후루의 생성 로직 변경
현재 목표 탕후루의 오브젝트는 Canvas의 하위에 생성되며, position을 설정하기 때문에, 해상도의 변경이 있을 경우, 목표 탕후루의 생성 지점도 유동적으로 움직일 수 없다.
그러한 문제를 해결하기 위해, 목표 탕후루 오브젝트의 생성을 아래의 패널의 하위에 생성되도록 하여 부모의 위치를 기준으로 y값만 조절하여 과일을 배치할 수 있도록 하였다.
부모 패널
// 'TargetTanghulu' GameObject 생성 및 'Image'의 자식으로 설정
GameObject targetTanghuluObject = new GameObject("TargetTanghulu");
Transform imageTransform = GameObject.Find("TanghuluUI").transform.Find("Image");
targetTanghuluObject.transform.SetParent(imageTransform, false);
결과적으로 목표 탕후루 이미지의 생성되는 위치를 정상화 할 수 있었고, 해상도의 문제도 해결되어 아래와 같이 극단적으로 정사각형의 해상도를 가질 경우에도 게임 플레이를 할 수 있게 되었다.
using System;
using System.Linq;
using System.Collections.Generic;
public class Solution {
public string[] solution(string[] players, string[] callings) {
string[] answer = new string[] {};
// 솔루션 1
// 1. 선수이름으로 등수를, 등수로부터 선수이름을 아는 사전형 두개를 준비
// 2. 선수 이름이 불리면
// 2-1. 해당 선수의 등수 확인 (ex. 5등)
// 2-2. 5등과 4등의 value값을 서로 바꾸며 이름 확인
// 2-3. 해당 이름을 가진 두 선수의 등수를 바꾼다
// 3. 모든 호명이 끝나면, 1등부터 answer에 입력
// 솔루션 2
// 다른 솔루션으로 생각해 볼 수 있는 건, ('5등이름','4등이름') 과 같이
// ('선수','앞선선수')의 key,value값을 가지는 사전형을 사용해 볼 수도 있을 듯.
// 이렇게 하면, 공간복잡도를 줄일 수 있다.
// => 파기. 실제 해보니 ('앞선수','선수','뒷선수')의 리스트형이 필요한데, 공간복잡도도 낮지 않고 복잡함.
// 1. 선수이름으로 등수를, 등수로부터 선수이름을 아는 사전형 두개를 준비
Dictionary<string,int> playerToNum = new Dictionary<string,int>();
Dictionary<int,string> numToPlayer = new Dictionary<int,string>();
for(int i=0;i<players.Length;i++){
playerToNum.Add(players[i],i);
numToPlayer.Add(i,players[i]);
}
// 2. 선수 이름 호명
foreach(string call in callings){
// 2-1. 해당 선수의 등수 확인
int num = playerToNum[call];
// 2-2. 해당 등수 선수와 앞등수 선수의 value값을 서로 바꾸며 이름 확인
string prePlayer = numToPlayer[num-1];
numToPlayer[num] = prePlayer;
numToPlayer[num-1] = call;
// 2-3. 해당 이름을 가진 두 선수의 등수를 바꾼다
playerToNum[prePlayer] = num;
playerToNum[call] = num-1;
}
// 3. 모든 호명이 끝나면, 1등부터 answer에 입력
// 검색 : Linq를 이용한 한줄 표현
answer = numToPlayer.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value).ToArray();
return answer;
}
}
솔루션 2번을 시도하려다가, '뒷 선수'의 정보도 리스트에 필요하다는 것을 알게 되어 포기
kvp는 KeyValuePair<int, string> 타입의 각 요소를 나타내며, NumToPlayer 딕셔너리의 각 항목을 순회하며 사용
=>는 람다 연산자라고 하며, 이를 기준으로 왼쪽은 입력 파라미터, 오른쪽은 함수 몸체를 나타냄
OrderBy(kvp => kvp.Key)를 호출하면, NumToPlayer의 각 KeyValuePair<int, string> 요소(kvp)를 kvp.Key (등수)를 기준으로 오름차순 정렬
event와 Action 특강
delegate, event, Action에 대해
delegate : 함수에 대한 참조 타입. 함수를 변수처럼 저장하거나 매개변수로 전달 할 수 있음.
delegate 반환형 델리게이트이름(매개변수);
event : delegate의 한 종류. 옵저버 패턴으로 활용 할 수 있다.
event 델리게이트이름 변수이름;
// 델리게이트 변수 앞에 event 키워드를 붙인다
Action : C#에서 제공하는 내장 delegate. 아래와 같이 두 줄을 한 줄로 줄여 쓸 수 있게 해 준다.
public delegate void MoveDelegateFunc(Vector2 moveVector);
public delegate void LookDelegateFunc(Vector2 lookVector);
public delegate void ShootDelegateFunc(bool fire);
public event MoveDelegateFunc OnMoveEvent;
public event LookDelegateFunc OnLookEvent;
public event ShootDelegateFunc OnFireEvent;
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
public event Action<bool> OnFireEvent;
단, Action은 반환형이 void인 델리게이트에만 사용이 가능하다.
구독 시스템
이렇게 event로 만든 변수에 여러 함수들을 추가 해 준 뒤
옵저버 역할을 하는 다른 클래스에서 특정 이벤트를 관측하여 해당 event 변수에 추가했던 메서드들을 모두 실행하도록 하는 패턴.
using System;
using System.Collections.Generic;
public class Solution {
public int[] solution(string today, string[] terms, string[] privacies) {
int[] answer = new int[] {};
// 프로그래머스에서 메서드 사용하는 법 및 출력메시지 띄우는 법 확인
// hello();
// 예상 솔루션
// 1. terms는 사전형 변수에 저장
// 2. 년도에는 365, 달에는 28, 일에는 1의 가중치를 두어 각 정보와 today를 비교
// 3. privacy가 같거나 작을 경우, result에 추가
// 3-1. result는 값 갯수의 변동이 있으므로 리스트로 관리
List<int> result = new List<int>();
// 1. terms를 사전형 변수에 저장
Dictionary<char,int> termsDict = new Dictionary<char,int>();
foreach(string term in terms){
// 검색: split 사용법(파이썬의 term.split())
// -> 마찬가지로 term.Split(' '); 과 같이 사용 가능
// 검색: 특정 인덱스 이후 모든 문자열을 사용하는법(파이썬의 term[2:])
// -> term.Substring(2); 과 같이 사용 가능
char key = term[0];
int value = int.Parse(term.Substring(2));
// Console.WriteLine($"{key}: {value}"); // 테스트 구문
// 검색: 사전형에 요소 추가
// -> termsDict.Add(key, value);
termsDict.Add(key, value);
}
// 2. 년은 365, 달은 28, 일은 1의 가중치를 두어 각 정보와 today를 비교
int today_value = 0;
today_value =
int.Parse(today.Substring(0,4)) * 28*12 +
int.Parse(today.Substring(5,2)) * 28 +
int.Parse(today.Substring(8,2)) * 1
;
Console.WriteLine($"Today value: {today_value}"); // 테스트 구문
for(int i=0; i<privacies.Length; i++){
string privacy = privacies[i];
int limitDay_value =
int.Parse(privacy.Substring(0,4)) * 28*12 +
int.Parse(privacy.Substring(5,2)) * 28 +
int.Parse(privacy.Substring(8,2)) * 1 +
termsDict[privacy[11]] * 28
;
Console.WriteLine($"privacy index[{i+1}] value: {limitDay_value}"); // 테스트 구문
// 3. privacy가 작을 경우, result에 추가
if(limitDay_value <= today_value)
result.Add(i+1);
}
answer = result.ToArray();
return answer;
}
public void hello(){
Console.WriteLine("Hello");
}
}
프로그래머스에서 메서드 사용하는 법 및 출력메시지 띄우는 법
문자열에서 특정 문자를 기준으로 쪼개는 법 // .Split(' ')
문자열 내에서 특정 인덱스 구간의 문자열을 떼어 사용하는 법 // .Substring(0,4)
팀 과제 '탕후루 만들기' -> (개명)'탕후루를 부탁해!' 의 대부분 기능을 구현하였다. 사운드쪽만 월요일까지 작업하는 것으로 하고, 나머지 시간은 QA에 할애 할 것으로 보인다.
팀 과제에 할애하느라 알고리즘 풀이를 거의 일주일만에 했는데 간단한 것도 생각이 안 나 찾아보며 진행했다.
2. 오늘 학습에 대해
오늘의 팀 프로젝트 커밋
한 게 정말 많지만, 기록하기에 할애할 여유가 없어 매일 아쉽다.
지금까지 진행하며 가장 아쉬운 점은 GameManager.cs에 너무 많은 량의 로직 코드를 작성했다는 것이다.
보기에도 지저분하고, GameManager에 대한 의존도가 심각하게 높다.
게임 전체를 총괄하는 역할은 맞지만, 세세한 공정까지 모두 직접 하는 애매한 포지션이 되어버렸다.
이러한 생각을 하며 대화형 인공지능에게 코드리뷰를 맡겨 본 결과는 아래와 같다.
유니티에서 GameManager와 같은 중앙 관리 클래스를 사용하는 것은 일반적인 패턴입니다. 이는 게임의 전반적인 상태 관리, 싱글톤 구현, 씬 전환, 점수 관리 등을 위해 사용될 수 있습니다. 하지만, 제공하신 코드를 보면 몇 가지 개선할 수 있는 부분이 보입니다:
책임 분리(Separation of Concerns): 현재 GameManager 클래스에는 여러 책임이 혼재되어 있습니다. 예를 들어, UI 관리, 게임 상태 관리, 씬 로딩, 점수 계산 및 고점수 관리, 사운드 관리 등이 모두 한 클래스 내에 구현되어 있습니다. 이러한 다양한 책임들을 별도의 클래스로 분리하여 각각의 관심사를 더 명확히 구분하는 것이 좋습니다. 예를 들어, UI 관련 로직은 UIManager, 점수 및 고점수 관리는 ScoreManager, 사운드 관리는 AudioManager와 같이 별도의 클래스로 분리할 수 있습니다.
긴 메소드 및 클래스:GameManager 클래스와 그 내부의 메소드들이 매우 길고 복잡합니다. 이는 코드를 읽고 이해하는 데 더 많은 시간이 소요되며, 유지 보수가 어려워질 수 있습니다. 각 메소드가 하나의 명확한 기능만 수행하도록 리팩토링하고, 필요하다면 복잡한 기능을 여러 개의 보조 메소드로 분리하는 것이 좋습니다.
정적 접근과 싱글톤:GameManager.Instance를 통해 전역적으로 접근 가능한 싱글톤 패턴을 사용하고 있습니다. 이는 편리할 수 있지만, 오버유즈는 전역 상태에 대한 의존성을 높이고 테스트와 유지보수를 어렵게 만듭니다. 싱글톤 사용을 최소화하고, 가능한 컴포넌트 간 의존성을 명시적으로 만드는 것이 좋습니다.
하드코딩된 값과 마법의 숫자: 코드 내에서 직접적으로 하드코딩된 값(예: "HighScore", "_MainScene" 등)과 마법의 숫자(예: 0.1f, 3 등)를 사용하고 있습니다. 이러한 값들은 상수로 정의하거나 구성 파일에서 불러오는 방식으로 관리하는 것이 좋습니다. 이렇게 하면 나중에 값을 변경할 때 한 곳에서만 수정하면 되므로 유지 보수가 용이해집니다.
리소스 관리:Resources.Load를 사용하여 리소스를 불러오는 방식은 유연하지만, 사용하지 않는 리소스가 메모리에 남아있을 수 있으므로 효율적인 리소스 관리를 위해 주의가 필요합니다. 가능하다면, 유니티의 Addressable Asset System 같은 더 효율적인 리소스 관리 시스템을 고려하는 것이 좋습니다.
게임 상태 관리: 게임 상태를 enum과 switch 문으로 관리하는 것은 간단한 프로젝트에는 적합할 수 있지만, 상태가 복잡해질수록 관리가 어려워집니다. 상태 패턴(State Pattern)을 적용하여 각 상태를 별도의 클래스로 구현하고, 이를 통해 상태 전환 로직을 더 명확하고 유지보수하기 쉽게 만들 수 있습니다.
코드를 개선하려면, 위의 지점들을 고려하여 리팩토링을 진행하는 것이 좋습니다. 이는 코드의 가독성, 확장성, 유지 보수성을 향상시키고, 장기적으로 프로젝트의 품질을 높이는 데 도움이 됩니다.
모든 씬에서 독립적인 클래스 상태를 가지는 싱글톤에 대해 배우며 적극적으로 활용하자고 생각했지만, 실제 프로젝트를 진행 해 보면 싱글톤이 마냥 좋은 것만은 아니었다. 특히 씬이 갱신 될 때마다 초기화를 해줘야 하는 번거로움을 싱글톤 스크립트에서 해결하려고 하면 코드가 무척 길어지고, 씬이 로드 되는 도중에 실행되지 않도록 별도의 작업도 하나하나 해 주어야 하는데 개발 과정에서 많은 버그를 일으키고 시간이 소비된다.
프로젝트의 전체 스크립트 파일
위와 같은 문제점들은 다른 스크립트에 책임분리를 함으로써 해결되는 부분이기 때문에, 스크립트 파일이 많아지는 것에 거부감을 갖지 않고 활용하면 좋겠다는 생각을 하였다.
내가 맡은 파트에서 GameManager.cs 와 UIManager.cs 에서 대부분을 해결했는데, DisplayMainPanel.cs 스크립트를 하나 추가하여 사용한 것 만으로도 숨통이 트이는 듯한 기분을 느꼈다.
알고리즘
오늘은 가볍게 한 문제만 풀이하였다. 별도의 메모를 해 가며 풀이하는 건 너무 많은 에너지가 소모되어, 코드를 작성하며 생각하고 참고한 내용들을 주석에 적는 방식을 채택하였다.
using System;
using System.Linq;
using System.Collections.Generic;
public class Solution {
public string solution(string[] survey, int[] choices) {
string answer = "";
// 일주일 쉬어서 감 다 죽음
// 찾아보기-1. choices의 각 원소를 3 빼서 새로운 배열에 담는 법(Linq)
// int[] newChoices = choices.Select(choice => choice - 4).ToArray();
// choices.Select(choice => choice - 4)를 사용했을 때의 결과 자료형은 IEnumerable<int>
// LINQ의 Select 메서드는 컬렉션의 각 요소에 대해 지정된 변환 함수를 적용하고
// 변환된 요소들을 포함하는 새로운 IEnumerable<T> 시퀀스를 반환
// 변환 함수 choice => choice - 4는 정수를 반환하기 때문에, 결과 시퀀스의 타입은 IEnumerable<int>
// 생각해보니, 위 방식은 설문결과의 분산(?)을 알 수 없게 만드는 요인이 되기 떄문에 사용하지 않도록 했다.
// 찾아보기-2. 사전형식 자료형 초기화
Dictionary<char, int> scores = new Dictionary<char, int>{
{'R',0},{'T',0},{'C',0},{'F',0},
{'J',0},{'M',0},{'A',0},{'N',0},
};
// var 사용과 Dictionary<char, int> 사용 중 자유롭게 선택. 장단점은 특별한 건 없고 상황이나 취향에 맞게.
// 각 문항별 점수 집계
int threshold = 4;
for(int i = 0; i < choices.Length; i++){
int selectedNum = choices[i];
if(selectedNum<threshold)
scores[survey[i][0]] += (threshold - selectedNum);
if(selectedNum>threshold)
scores[survey[i][1]] += (selectedNum - threshold);
}
// 성격유형 진단
if(scores['R']>=scores['T']) answer += 'R'.ToString();
else answer += 'T'.ToString();
if(scores['C']>=scores['F']) answer += 'C'.ToString();
else answer += 'F'.ToString();
if(scores['J']>=scores['M']) answer += 'J'.ToString();
else answer += 'M'.ToString();
if(scores['A']>=scores['N']) answer += 'A'.ToString();
else answer += 'N'.ToString();
return answer;
}
}
코드를 간결하게 하는 Linq, 사전형의 사용을 다시 찾아보았다.
탭을 활용하여 반복되는 코드를 깔끔하게 정리했다.
탭을 활용하는 건 처음 시도 한 방법인데, 이전 팀원 중 한명의 코드 작성 방식이 떠오른 겸 적용 해 보았는데 깔끔하게 작성된 것을 보고 흡족했다.
팀 과제 탕후루 만들기 2일차이다. 기본적인 게임진행 로직, 장면디자인과 상호작용을 오늘 대부분 완료한 것으로 보인다.
다만 자잘한 버그가 정말 많고 하나하나 별 거 아닌 기능인데 해결이 안되어 아주 골칫거리이다.
오는 수요일까지 과제제출 및 결과 발표라 생각보다 여유 시간이 될 지 모르겠다.
오후에는 챌린지 클래스 세션이 있었다. 델리게이트에 관한 소개와 예시로 세션을 진행하였다. 전혀 몰랐던 내용이긴 하지만 파이썬에서도 비슷한 방식으로 함수를 사용했기 때문에 흡수하는 데에 무리는 없었다.
2. 오늘 학습에 대해
[Serialized] 에 대해 새로 배운 사용법
팀원 한명이 에디터에서 플레이 버튼을 누르고 [Serialized]를 통해 변수가 바뀌는 걸 보며 디버깅을 한다는 얘기를 들었다. 생각 못 해본 방법이었는데 꽤나 괜찮다고 생각했다.
싱글톤 오브젝트의 치명적인 단점
예를 들어 ButtonManager.cs에서 '패널을 활성화하는 메서드'를 작성한 후, 다른 씬의 버튼에서 이걸 사용하려고 한다면 에디터의 버튼 이벤트로 작용하는 OnClick에 ButtonManager 오브젝트를 드래그 할 수가 없어, 스크립트로 할당하는 게 강제되는 것으로 보인다.
구글링이랑 GPT를 통해 알아봐도 스크립트의 구성을 다르게 하는 것 말고 깔끔한 해결법은 없는 것으로 보인다.
싱글톤 오브젝트를 사용 시 문제점 두번째
씬 전환 등을 할 때 Start나 Awake가 실행되지 않기 때문에 다른 방안을 모색해야 했다.
씬의 로딩이 완료되고 다른 오브젝트들이 만들어진 후 스크립트를 실행하는 방법의 예는 아래와 같다.
private void Start()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
FindAndAssignUIElements(scene);
}
private void FindAndAssignUIElements(Scene scene)
{
// 로드되는 씬 이름에 따라 UI를 로드
if (scene.name == "_TitleScene")
{
GameObject canvasObject = GameObject.Find("Canvas");
highscore = GameObject.Find("HighScore").GetComponent<Text>();
settingsPanel = GameObject.Find("SettingUI");
}
else if (scene.name == "_MainScene")
{
GameObject canvasObject = GameObject.Find("Canvas");
pausedPanel = canvasObject.transform.Find("PauseUI").gameObject;
settingsPanel = GameObject.Find("SettingUI");
score = canvasObject.transform.Find("ScoreUI").transform.Find("Score").GetComponent<Text>();
resultPanel = canvasObject.transform.Find("ResultUI").gameObject;
resultScore = canvasObject.transform.Find("ResultUI").transform.Find("ScoreText").GetComponent<Text>();
resultHighScore = canvasObject.transform.Find("ResultUI").transform.Find("HighScoreText").GetComponent<Text>();
}
// 씬 로드 이벤트 발생할 때마다 초기 상태를 비활성화로 설정
SetPanelActive(settingsPanel, false);
SetPanelActive(pausedPanel, false);
SetPanelActive(resultPanel, false);
}
씬의 이름에 따라 여러 UI 오브젝트들을 UIManager에 할당해준다. 다만 이런 방식은 UIManager가 처음 생성될 때는 작용하지 않는 것으로 보인다. SettingUI 오브젝트를 어째선지 다른 방법을 동원해도 UIManager에 할당하지 못하여 아직도 타이틀메뉴에서 세팅 창을 열지 못하는 상태이다. 정말 단순한 방법으로도 되지 않은 것으로 보아 무언가 알 수 없는 꼬임이 발생한 것 같다.
위와 같이 프로젝트 루트 폴더에 .editorconfig 파일을 만들어 인코딩 문제를 해결하는 방법이 있던데, 현업에서도 이런 밑작업을 하고 시작하는 편인가요? 몇 주 전에 팀 프로젝트에서 이슈(주석이나, '한글이름.png'를 불러오는 코드 등)가 있었을 때에는 코드 내에 한글 사용을 지양하는 걸로 진행했습니다.
맥을 사용하는 사람이 없다면, 그냥 세팅 없이도 하는 편.
실제로 한글 사용을 지양하는 편이 맞는듯.
(제일 처음 질문에 이어)Stash에 관해
Branch - Stash all changes // 임시저장 후 다른 브런치로 이동(체크아웃) 등 할 수 있음
Restore을 통해 Stash했던 내용 가져오기 가능
stash 공간은 하나인가요?
확인 필요(확인 안되고 끝난듯)
체리픽에 관해
특정 커밋만 선택해서 가져오는법
Github Desktop에서도 가능
혹시 이미 커밋해버린 친구들을 지우는 방법이 있나요?+커밋 이름도 커밋후에 수정이 가능한지 궁금합니다
Push가 된 건 못돌림.
아직 커밋만 된 건 취소 가능 : Undo Commit
Amend commit(가장 마지막 푸시된 Commit만 가능) + Forced Push Origin : 이미 푸시한 커밋도 '커밋 메시지'만은 수정 가능
Revert?
conflict 도중에 a작업물을 선택할거를 잘못해서 b를 선택해서 머지 되었을 경우 다시 conflict 작업으로 돌아가서 b작업물을 선택할 수 있는 방법이 있나요?
Unity를 사용하여, 캐릭터 외형과 이름선택을 하여 필드에서 돌아다닐 수 있도록 하는 개인과제 프로젝트를 진행중이다.
제출기한이 오늘까지인데, 1차제출인 13시까지 밤을 새서 만들어 제출하고, 피곤해서 꾸벅꾸벅 졸면서 오후를 보내고, 추가 보수를 하여 21시에 다시 제출하였다.
개발 초기에 스크립트 간 설계를 제대로 하지 않아 현재 시점에서는 보수가 크게 필요한 상황이라는 점, 특히 플레이어의 데이터가 여러 곳에 산재해 있어 이를 모아 하나의 클래스 객체화 하는 것이 남은 과제이다. 시간을 들여서 보수를 할 수 있을 것 같은데, 내일부터 할 팀 프로젝트는 아무래도 개인프로젝트에서 이어지지 않는다는 소문이 있어 추가 작업을 할 것 같지는 않다.
2. 오늘 학습에 대해
개발 중 이슈에 대해서는 리포지토리의 README.md에 모두 작성하였다. 그 내용은 아래와 같다.
필수 요구사항
캐릭터 만들기
입문 강의 중 사용했던 스프라이트 사용
캐릭터 이동 및 바라보기
InputSystem 사용
스테이지 만들기
타일맵 사용
카메라가 캐릭터를 따라가기
카메라 오브젝트를 Player의 하위 오브젝트로 넣음.
스무스한 카메라 구현은 현재 방식으로 어려움, 이후 CameraPivot 오브젝트를 Player의 하위 오브젝트로 넣어 MainCamera가 정해진 규칙대로 CameraPivot을 따라가도록 하여 개선 가능
캐릭터 애니메이션
현재 준비된 두 캐릭터에 대해 각각 idle상태, walk상태에 대한 애니메이션 마련
Animator의 파라미터로 int CharacterNum을 주어, 캐릭터의 외형을 선택 시 해당 CharacterNum 를 설정하고 그에 맞는 애니메이션을 Layers에서 선택하여 재생하려고 하였으나, 잘 작동하지 않아 한 개의 레이어에 두 캐릭터 외형에 대한 애니메이션을 모두 넣음. 캐릭터 외형 증가에 따라 애니메이터 동작 업데이트의 확장성 매우 낮음.
시작 시 이름 입력
2~10자 이내로 이름 입력 가능
캐릭터의 머리 위에 이름 반영
시작 시 캐릭터 선택
두 종류의 캐릭터 마련, 캐릭터 종류가 늘어날 때마다 애니메이션을 추가해야함. 현재 애니메이터 구조로는 다수의 캐릭터 지원 어려움
선택 요구사항(달성)
시간 표시
TMP와 LegacyText 어느쪽을 사용하더라도 시간 표시가 반영되도록 스크립팅
인게임 캐릭터 재선택
개선 할 점
타일 맵 분리 작업 : 현재 장식과 BackDesign이랑 같이 그려진 상태, 장식 타일만을 배치할 Decorate 타일맵 오브젝트 생성으로 개선 필요
텍스트 폰트 단일화 : TMP에는 나눔글씨체를 사용하지 못하고 있음, Legacy로 바꾸던가 TMP용 폰트파일을 만들던가(시간 오래걸림) 해서 서체 단일화 필요.
현재 산재한 플레이어 정보들을 모아 하나의 클래스 객체로 관리하기
위와 동시에 스크립트도 정리하기
TMP와 LegacyText 어느쪽이라도 텍스트가 반영되도록 하는 전용 유틸 메서드 작성
Image와 SpriteRenderer 어느쪽이라도 스프라이트가 반영되도록 하는 전용 유틸 메서드 작성
해야 할 일
Player의 클래스화
NPC와 Player의 클래스 리스트 관리
선택과제 2번 : 인게임 이름 바꾸기(새로운 캔버스 또는 패널을 만들어야 할 듯)
선택과제 3번 : 참석 인원 UI(NPC/Player의 리스트 관리 마련 후 가능)
선택과제 5번 : NPC 대화(NPC 마련부터 해야함)
개발이슈
입문 강의를 소화하고 개인과제까지 하는 데에 너무 적은 시간이 부여됨. 강의시청을 도중 중단하고, 1일 이내의 시간을 개인과제에 할애하여 필수 요구사항만을 우선 구현.
개인과제를 급하게 만들었기 때문에 강의에서 배운 내용을 적용하지 못한 부분이나 플레이어 데이터의 관리 등 여러 부분에서 결함이 많음.
TextMeshPro에서 새로운 폰트 적용이 번거로움. 한글 폰트를 새로 작성하고 적용하려 했으나 생성에 시간이 너무 오래 걸려 보류.
3. 과제에 대해
진행중인 과정에 대해 상기한대로, 추가적인 프로젝트 보수는 없을 것 같다. 내일부터 새로 주어지는 팀 과제에 착수.
주어진 유니티 강의를 50%가량 소화하지 못했다. 해당 강의를 다시 보며 공부해야 할 것 같다. 디자인패턴에 대해 신경을 쓴 내용이라 필수적으로 알아야 할 스킬들이다.
강의 난이도가 너무 높아 50% 부근까지만 소화한 후, 개인과제에 착수하였다. 내일 정오까지 제출을 해야하기 때문에 시간 부족으로 어쩔 수 없는 선택.
개인과제도 커스텀하기는 어려울 것 같고, 필수 구현요소 7개만 일단 제대로 구현하기로 생각했다.
캐릭터 만들기, 타일맵으로 플레이 공간 만들기, 카메라가 캐릭터를 따라가기, 캐릭터 이름 입력하기, 캐릭터 외형 선택하기의 구현을 마쳤고, 캐릭터이동과 idle 애니메이션을 구현하면 일단 필수요소의 구현은 끝이다. 현재 오후 9시, 남은 요소는 내일 오전까지의 시간을 활용해서 구현해야 할 듯 하다.
강의에서 배운 내용을 활용하기에는 시간이 촉박하여 스크립트 간 디자인 없이 깡으로 구현하였다.
2. 오늘 학습에 대해
유니티 인스펙터에서 한글 깨짐 현상
유니티 인스펙터에서 한글이 깨져보인다
위 처럼 유니티에서 깨져보이는 현상 이외에도, 이대로 깃허브에 올리고 팀원에 공유를 하면 거기에서는 편집기에서조차 깨져보이기도 한다.
해결방법은, 해당 프로젝트의 루트 폴더(.sin 파일이 위치한 곳)에 인코딩을 지정해주는 파일 하나를 생성하면 된다.
[*]
charset = utf-8-bom
메모장을 열어 위와 같이 작성 후, .editorconfig 라는 이름으로 루트 폴더에 저장 해 준다.
.editorconfig가 저장된 모습
인코딩 설정을 이렇게 하고, Visual Studio 편집기에서 한 번 더 저장을 해준다.
C#문법과 콘솔게임 주차에서 Unity로 다시 돌아와 개인과제주차를 진행하고있다. 입문 과정이라고 지급된 유니티 강의 영상이 생각보다 어려운 내용이었다. Docs문서를 확인하면 어제 하루간 이 강의를 완강하고 나머지 3일간 개인과제를 하는 것을 염두에 둔 계획을 적어두었는데, 오늘 늦은 오후에 강의 완료표시만 모두 찍어두고 다시 이어서 소화하자고 생각하여 진행도를 100% 찍었는데
어.. 아무도 완강을 못한 상태인가보다. 하루는 커녕 이틀이 끝나가는데 이 상태라면, 운영측의 스케쥴 계획에서 큰 미스가 아닌가 생각했다.
어쨌든 오늘도 유니티 강의를 들으며 하루를 보냈다. 어렵긴 하지만 게임 개발을 위한 오브젝트와 스크립트 구조에 대해 심도가 무척 깊어 유익한 내용이다.
그리고 챌린지/노말/이지 였나로 수강생의 역량에 맞춰 따로 세션을 진행한다고 한다. 화요일과 목요일 주 2회 예정인데, 구체적으로 어떤 방식으로 진행하는지는 모르겠지만 최적화와 메모리 관리 등 듣기만해도 무서운 내용이 기다리고 있는 것은 확실한 듯.
2. 오늘 학습에 대해
스크립트 관련하여 들은 내용이 거의 새롭게 배운 부분이기 때문에, 주어진 시간이 촉박하여 정리하면서 듣기는 좀 어려웠나 생각한다.
타일맵
RPG 메이커 비슷한 느낌으로 유니티에서도 맵을 구성하는 타일과 벽 등을 쉽게 그리는 기능이 있었다.
위와 같이 생긴 윈도우에서 색깔을 선택하듯이 타일을 선택하고, 하이에라키에 추가했던 오브젝트에 도트 아트를 하듯이 타일을 그려 구성하는 느낌.
하이에라키에서는 2D Object - Tilemap - Rectangular 등을 통해 타일 오브젝트를 만들고
상단메뉴의 Window - 2D - Tile Palette 윈도우를 열어, 새로운 팔레트를 생성, 바닥 등의 스프라이트 등을 팔레트에 추가하여 사용한다.
새로운 팔레트를 위와 같이 만들면, 해당 팔레트를 어디에 저장할지 폴더를 지정하도록 한다. 나는 강의를 따라하며 Assets/Artworks/Level/ 을 지정해주었다.
Tile Palette에 타일 스프라이트를 드래그하여 추가하였다타일을 추가하며 저장된 데이터
그렇게 만들어진 팔레트에, 앞으로 사용할 바닥이나 벽 길가의 풀숲 등 타일로 사용할 스프라이트들을 드래그하여 추가한다. 여러개 선택해서 드래그하면, 적당히 정사각형에 가까운 예쁜 배치를 해 준다. 스프라이트를 드래그하는 시점에 마찬가지로 저장 위치를 지정하도록 하는데, 역시 강의를 따라하며 Assets/Artworks/Level/Tiles/ 위치에 저장하도록 하였다.
타일 오브젝트 Floor에 바닥을 를 그리고있다BackDesignForeDesign
하이에라키에서 2D Object - Tilemap - Rectangular 를 통해 타일맵 오브젝트를 만들고, 여러 종류의 타일들을 작성해준다.
어느 타일 오브젝트가 Player를 가리는지, 타일 오브젝트끼리 서로 가려지는 관계에 따라 Floor, BackDesign, ForeDesign 오브젝트를 작성하였고, 각각 높이(Order in Layer)를 0, 2, 20 으로 설정하였다.
그리고 플레이어가 지나가지 못하도록 하기 위해 Collision 도 이 Tilemap을 통해 만든다.
Order in Layer가 0이어서 사진에는 여러가지 타일을 쓴 것 같아 보이지만, 가려서 그렇게 보일 뿐 두세가지 종류의 타일을 사용하여 위와 같은 범위에 Collision 용 타일을 그려주었다. 그리고 Tilemap Collider 2D 컴포넌트를 추가해주고
색을 투명하게 바꿔주어 충돌체의 역할만 수행하도록 한다. 마찬가지로 Player에게도 Box Collider 2D 컴포넌트를 추가한다.
위와 같은 결과가 나오게 되었는데, 보니까 Player가 BackDesign에 가려지는 것이 보여, 0으로 설정되어있던 Player의 Order in Layer를 5로 바꿔주었다.
같은 이름 변수명 이름 한번에 바꾸기
Visual Studio 에서 위와 같이 같은 이름의 변수명 등을 한번에 바꿔줄 수 있다.
단축키는 Ctrl R R
최신 버전의 Visual Studio는 F2 키로 사용할 수 있다고 한다.
3. 과제에 대해
강의 내용 흡수
개인과제 완수
4. 참고자료
없음
5. GPT4 문답
리스트에서 특정 인덱스 범위 삭제
[You]
List<int> stackAll = new List<int>();
...
//현재 List : 1, 2, 3, 4, 5, 6, 7 ,8 ,9
현재 리스트에서 6에서 9를 날려버리고 1에서 5만 남기도록 하는 법
[ChatGPT]
List<T>에서 특정 범위의 요소를 제거하려면 RemoveRange 메서드를 사용할 수 있습니다. 이 메서드는 지정된 인덱스에서 시작하여, 지정된 개수만큼의 요소를 리스트에서 제거합니다.
예를 들어, 리스트 {1, 2, 3, 4, 5, 6, 7, 8, 9}에서 6부터 9까지의 요소를 제거하려면, 6의 인덱스를 찾고(여기서는 5), 그 인덱스부터 리스트 끝까지의 요소를 제거해야 합니다. List<int>의 인덱스는 0부터 시작하기 때문에, 6의 인덱스는 5가 됩니다.
따라서, 다음과 같이 RemoveRange 메서드를 사용할 수 있습니다:
List<int> stackAll = new List<int> {1, 2, 3, 4, 5, 6, 7, 8, 9};
stackAll.RemoveRange(5, 4); // 5번 인덱스부터 4개의 요소를 제거합니다.
여기서 5는 제거를 시작할 인덱스이고, 4는 제거할 요소의 개수입니다. 이렇게 하면 리스트에서 6, 7, 8, 9가 제거되고 1, 2, 3, 4, 5만 남게 됩니다.
[You]
stackAll.RemoveRange(5); 와 같이 사용해서 Index 5 이후의 원소는 모두 날려버리던가 하는 방법은 어렵니?
[ChatGPT]
List<T>의 RemoveRange 메서드는 두 개의 인자를 필요로 합니다: 제거를 시작할 인덱스와 제거할 요소의 개수입니다. RemoveRange 메서드는 인자 하나만 사용하는 오버로드가 없기 때문에, 인덱스 5 이후의 모든 원소를 제거하려면 다음과 같이 제거할 요소의 총 수를 계산해야 합니다:
List<int> stackAll = new List<int> {1, 2, 3, 4, 5, 6, 7, 8, 9};
int removeCount = stackAll.Count - 5; // 제거할 요소의 수를 계산합니다.
stackAll.RemoveRange(5, removeCount); // 인덱스 5부터 removeCount만큼의 요소를 제거합니다.
이 코드에서 stackAll.Count - 5는 리스트의 총 길이에서 5를 뺀 값이며, 이는 인덱스 5 이후의 모든 요소의 수를 의미합니다. 이렇게 계산한 값을 RemoveRange의 두 번째 인자로 사용하면 인덱스 5부터 리스트의 끝까지 모든 요소가 제거됩니다.
글자 서식을 정돈하기 위해, 티스토리의 글 작성법을 마크다운 위주로 바꾸었다. 특히 외부에서 마크다운 형식의 컨텐츠를 가져올 때 그대로 가져오기 위해서는 마크다운 모드를 쓰지 않으면 안되기 때문.
기본모드/마크다운/HTML 모드를 오가는 도중에 손실되는 정보가 상당하기 때문에(특히 마크다운으로 전환 시, 이미지 속성등이 모두 사라지는 등, 마크다운에서 지원하지 않는 작성법은 모두 손실된다) 개발자로서 가장 필요로 하는 마크다운을 메인으로 하고, 필요 시 기본모드로 넘어가 추가적인 편집을 할 생각이다.
새로운 6시간정도 분량의 새로운 유니티 입문 강의를 지급받아, 4일 간 강의를 보며 공부하고 개인과제를 하는 주차이다. 오늘 셔플한 새로운 조는 이전 조에 비해 많이 조용한듯.
오후 3시정도까지는 너무 피곤하여 졸다 깨다를 반복하다가 이후에서야 강의를 확인하며 학습했다. 강의 난이도가 생각보다 높은 것을 보고 당황하였다. 저녁을 먹고 와서도 다들 조용히 자기 할 일 하는 중. 일단 강의를 두시간 분량(약 1/3정도?) 듣기는 했는데, 학습과 과제까지 하는 시간이 4일밖에 주어지지 않아 시간이 많이 빡빡한 편이다.
이제 보니, 오늘은 강의를 수강하고 나머지 시간은 개인과제를 하도록 캠프 Docs에 적혀있다. 오늘 발제 세션이 끝난 뒤의 실 학습 가능한 시간이 식사시간과 일지 작성하는 시간 제외하고 6시간정도 였을텐데, 6시간의 강의를 이 시간에 소화하라는 것은 상당히 무리가 있지 않나 생각했다.
2. 오늘 학습에 대해
Input 요소의 설정 변경
아래와 같은 키입력을 유니티 스크립트에서 사용 할 수 있는데
float x = Input.GetAxis("Horizontal");
Edit - Project Settings - Input Manager 에서 Horizontal, MouseX 등의 이름 변경이나 상세설정 등을 할 수 있다.
GetAxis와 GetAxisRaw
Input.GetAxis("Horizontal") 와는 달리 Input.GetAxisRaw("Horizontal") 는 스무스한 변화(-1 ~ 0 ~ 1) 없이 입력 순간부터 등속(-1,0,1)으로 움직이게 한다.
*C# 문법, ?.
OnMoveEvent?.Invoke(direction); 와 같이 ?. 을 사용하면, OnMoveEvent가 Null이 아닐 경우, Invoke(direction)을 실행한다.
유니티 라이센스 오류 해결
수강을 위해 유니티 버전을 2022.3.2f로 낮춘 후, 패키지 관리자를 사용하려고 보니 라이센스 이슈가 있었다.
라이센스 문제로 보여, 유니티 허브에서 라이센스 관리 메뉴로 이동.
이처럼 Refresh 버튼을 누르면 라이센스의 갱신을 할 수 없다는 내용이 나온다.
유니티 라이센스 Data 삭제 : 해당 스레드에서 유니티 데이터폴더를 지운 후 해결되었다는 덧글들이 많아 해보았지만 해결되지 않았다.
라이센스 정보가 사라진 것은 덤.
최하단 댓글 중 Unity Hub의 로그인을 다시 해서 해결되어다는 내용을 보고, 재로그인을 해 보았더니 해결되었다.
라이센스 시작일이 꽤 오래전이라 뭔가 아깝다는 생각이 들어 복원방법을 찾아보다가 포기한 후, 새로 무료 라이센스를 얻기를 하였는데, 다행히 시작일이 예전에 쓰던 라이센스와 동일했다.
유니티 프로젝트 이름을 정하고 있는데, 보통의 경우 여러 단어로 이루어진 이름을 지을 때 대소문자 등 작명 규칙을 어떻게 사용하니?
[ChatGPT]
Unity 프로젝트의 이름을 정할 때, 일반적으로 "카멜 케이스(CamelCase)" 또는 "파스칼 케이스(PascalCase)"와 같은 작명 규칙을 사용합니다. 이러한 규칙은 여러 단어를 결합할 때 각 단어의 시작을 대문자로 표시하여 단어 간의 구분을 명확하게 합니다. 다음은 두 가지 주요 작명 스타일에 대한 설명입니다:
카멜 케이스 (CamelCase): 이 방식에서는 첫 단어를 소문자로 시작하고 이후의 각 단어는 대문자로 시작합니다. 예를 들어, myUnityProject와 같이 사용됩니다. 카멜 케이스는 주로 변수나 함수 이름에 사용됩니다.
파스칼 케이스 (PascalCase): 이 방식에서는 모든 단어를 대문자로 시작합니다. 예를 들어, MyUnityProject와 같이 사용됩니다. 파스칼 케이스는 클래스 이름, 네임스페이스, 프로젝트 이름 등에 자주 사용됩니다.
프로젝트 이름을 지을 때는 일반적으로 파스칼 케이스를 사용하는 것이 좋습니다. 이는 프로젝트 이름이 종종 클래스나 네임스페이스와 유사한 형태로 사용되기 때문입니다. 또한, 프로젝트 이름은 의미를 명확하게 전달할 수 있도록 간결하고 기억하기 쉬운 이름을 선택하는 것이 좋습니다.
예시:
SpaceAdventureGame
VirtualRealitySimulator
MobilePuzzleGame
이러한 규칙은 프로젝트를 더욱 조직적이고 일관된 방식으로 관리하는 데 도움이 됩니다. 프로젝트 이름을 결정할 때는 프로젝트의 내용과 목적을 잘 나타낼 수 있는 이름을 선택하는 것이 중요합니다.
Input System 에 대해
[You]
현재 Player에 다음 스크립트 컴포넌트가 추가되어 있어.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerInputController : TopDownCharacterController
{
private Camera _camera;
private void Awake()
{
_camera = Camera.main;
}
public void OnMove(InputValue value)
{
Vector2 moveInput = value.Get<Vector2>().normalized;
CallMoveEvent(moveInput);
}
public void OnLook(InputValue value)
{
Vector2 newAim = value.Get<Vector2>();
Vector2 worldPos = _camera.ScreenToWorldPoint(newAim);
newAim = (worldPos - (Vector2)transform.position).normalized;
if (newAim.magnitude >= .9f)
{
CallLookEvent(newAim);
}
}
public void OnFire(InputValue value)
{
Debug.Log("OnFire" + value.ToString());
}
}
그리고 Player에는 Player Input이라는 컴포넌트도 추가되어있는데, Actions 속성에 TopDownController2D 라는 InputAction 을 넣어놓은 상태야.
Behavior은 Send Messages로 설정되어있고, 아래 적혀있는 설명을 보면 Will SendMessage() to GameObject: OnDeviceLost, OnDeviceRegained, OnControlsChanged, OnMove, OnLook, OnFire 라고 적혀있어.
PlayerInputController라는 스크립트에 존재하는 OnLook, OnMove, OnFire 라는 메서드를 어떤 식으로 실행시키는지 연결고리가 어떻게 되어있는지 도무지 이해가 되지 않아.
다른 스크립트도 참고삼아 적어둘게.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TopDownCharacterController : MonoBehaviour
{
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
public void CallMoveEvent(Vector2 direction)
{
OnMoveEvent?.Invoke(direction);
}
public void CallLookEvent(Vector2 direction)
{
OnLookEvent?.Invoke(direction);
}
}
Unity의 PlayerInput 컴포넌트와 관련된 스크립트들이 어떻게 연결되어 있는지 궁금해하시는군요. Unity의 입력 시스템, 특히 PlayerInput 컴포넌트는 약간 복잡할 수 있지만, 기본적으로 이해하면 간단합니다. 각 부분을 단계별로 살펴보겠습니다.
PlayerInput 컴포넌트
PlayerInput 컴포넌트는 Unity의 새로운 입력 시스템의 핵심입니다. 이 컴포넌트는 입력을 처리하고, 입력에 대응하는 이벤트를 발생시킵니다. 여기서 중요한 것은 Behavior 설정입니다. "Send Messages" 옵션은 PlayerInput 컴포넌트가 입력 이벤트를 감지할 때, 이벤트에 해당하는 이름의 메서드를 해당 게임 오브젝트의 스크립트에서 호출하도록 합니다.
PlayerInputController 스크립트
이 스크립트에는 OnMove, OnLook, OnFire라는 메서드들이 정의되어 있습니다. 이 메서드들은 PlayerInput 컴포넌트에 의해 호출됩니다. 예를 들어, 사용자가 이동 키를 누르면 PlayerInput 컴포넌트는 OnMove 메서드를 호출합니다.
OnMove 메서드는 입력 값을 받아서 이를 CallMoveEvent 메서드에 전달합니다. 이렇게 하면 TopDownCharacterController 클래스에 정의된 OnMoveEvent 이벤트가 발생합니다.
TopDownCharacterController 스크립트
이 스크립트는 OnMoveEvent와 OnLookEvent라는 이벤트를 정의합니다. 다른 스크립트는 이 이벤트에 메서드를 등록할 수 있고, 이벤트가 발생하면 등록된 메서드가 호출됩니다.
TopDownMovement 스크립트
이 스크립트는 TopDownCharacterController의 OnMoveEvent에 Move 메서드를 등록합니다. 그래서 PlayerInputController에서 OnMove 메서드를 호출하여 OnMoveEvent 이벤트를 발생시킬 때, TopDownMovement의 Move 메서드가 호출됩니다.
연결 고리
요약하면, 사용자의 입력은 PlayerInput 컴포넌트에 의해 감지되고, PlayerInputController 스크립트의 메서드를 호출합니다. 이 메서드들은 TopDownCharacterController의 이벤트를 발생시키고, 다른 스크립트에서 이 이벤트에 등록된 메서드들이 호출됩니다. 이 과정을 통해 입력이 실제 게임 내 행동으로 변환됩니다.
[You]
사용자의 입력은 PlayerInput 컴포넌트에 의해 감지되고, PlayerInputController 스크립트의 메서드를 호출합니다. 라고 적혀있는데, PlayerInputController 가 해당 오브젝트의 컴포넌트로 추가되어있다는것 만으로, PlayerInput 는 PlayerInputController의 안에있는 OnMove 등의 메서드에 바로 접근하는거야?
[ChatGPT]
네, 맞습니다. Unity의 새로운 입력 시스템에서 PlayerInput 컴포넌트는 입력과 관련된 이벤트를 감지하고, 이를 스크립트에 연결된 메서드로 전달합니다. 이 연결은 PlayerInput 컴포넌트의 설정과 스크립트에 정의된 메서드 이름에 의해 이루어집니다.
PlayerInput 컴포넌트는 "Behavior" 설정을 "Send Messages"로 설정했을 때, 다음과 같이 작동합니다:
감지된 입력에 대응하는 메서드 이름 확인: 예를 들어, PlayerInput 컴포넌트의 "Actions" 설정에 "Move"라는 액션이 정의되어 있고, 사용자가 이동 입력을 하면, PlayerInput 컴포넌트는 "OnMove"라는 이름의 메서드를 찾습니다.
메서드 호출: 만약 PlayerInput 컴포넌트가 추가된 게임 오브젝트에 PlayerInputController 스크립트가 있고, 그 안에 "OnMove"라는 메서드가 정의되어 있다면, PlayerInput 컴포넌트는 이 메서드를 자동으로 호출합니다.
이 과정은 PlayerInput 컴포넌트가 스크립트의 메서드 이름을 기반으로 자동으로 연결을 수행한다는 점에서 중요합니다. 따라서, PlayerInput 컴포넌트의 액션 이름과 스크립트의 메서드 이름이 일치해야 올바른 연결이 이루어집니다. 예를 들어, "Move" 액션이 있으면, 스크립트 내에 "OnMove"라는 메서드가 정의되어 있어야 합니다.
[You]
즉, PlayerInput은 게임 오브젝트 내의 모든 컴포넌트를 뒤적여서 OnMove 메서드를 찾는다는거지? 아니면 PlayerInputController라는 이름을 가진 컴포넌트 내만 탐색한다던가?
[ChatGPT]
Unity의 PlayerInput 컴포넌트는 "Send Messages" 동작 모드를 사용할 때, 해당 게임 오브젝트에 부착된 모든 컴포넌트를 대상으로 메서드를 탐색합니다. 즉, PlayerInput 컴포넌트는 특정한 스크립트 이름(예: PlayerInputController)을 찾는 것이 아니라, 게임 오브젝트에 부착된 모든 컴포넌트 안에서 해당 이벤트와 일치하는 메서드 이름(예: OnMove)을 찾습니다.
이 과정은 다음과 같습니다:
입력 이벤트 감지: 사용자의 입력이 PlayerInput에 의해 감지됩니다.
메서드 탐색: PlayerInput 컴포넌트는 해당 게임 오브젝트에 부착된 모든 컴포넌트를 검사하여, 입력 이벤트와 일치하는 메서드 이름을 찾습니다. 예를 들어, 이동 입력에 대해서는 OnMove라는 메서드를 찾습니다.
메서드 실행: 일치하는 메서드가 발견되면, PlayerInput 컴포넌트는 그 메서드를 호출합니다.
따라서, PlayerInputController라는 특정한 이름의 스크립트를 찾는 것이 아니라, 입력 이벤트에 대응하는 메서드를 게임 오브젝트에 부착된 모든 컴포넌트에서 찾아 호출합니다. 이렇게 하면, 여러 스크립트가 같은 게임 오브젝트에 존재하더라도 각각의 스크립트에서 필요한 입력 처리를 할 수 있습니다.