2024.02.20 현재 부분을 작성하는 시점, 전문 DAW를 사용하는 방법 외에 다른 수단을 찾아보지 않았습니다.
인터넷에서 여러가지 음악과 노래의 midi파일을 구할 수 있고, 이를 바탕으로 다른 악기로의 연주, 여러가지 이펙트를 넣어 사용할 수 있지만, 그 전에 원본 midi를 높낮이 반전 또는 shift pitch만을 간단하게 하여 새로운 느낌의 소스로 선행작업을 해 줄 수 있다고 생각하였습니다.(물론 DAW에서는 정말 간단하게 두 가지 작업을 할 수 있으나, 여기서는 DAW를 사용하지 않고 선행작업을 하는 방법을 생각합니다.)
팀 프로젝트 3/5(발표날 포함)일째이다. 3D플랫폼 퍼즐 게임을 진행중이고, UI 및 씬 관리 전반을 하고있다.
굉장히 시간이 모자라다. 스크립트와 UI의 기본적인 프리팹은 모두 준비되었는데, 메인 브런치에 Merge를 해가며 진행할 요소가 너무 많아 시간에 맞출 수 없을 것 같은 기분이 들고, UI가 아니더라도 퍼즐 쪽도 어떻게 진행이 될지, 내일 하루만에 완성을 해야할텐데 정말 모르겠다.
Todo 리스트를 열심히 지워가며 진행중인데도 아직 할 게 많이 남았고 많이 또 생길 것 같다.
Todo
현재 Data에 따라 Stage 해금되도록 하기, 스테이지 선택 할 수 있도록 하기
애니메이션이나 Fade in out 효과 넣기
전체적인 디자인 손보기
Merge 후 손보기
ESC키로 UI_Pause 띄우게 하기 : 제대로 메서드 써서 씌워야 Popup 오더 관리 됨
Player가 상호작용 가능 할 때, 어떻게 UI를 보여줄지 생각
Player의 상태를 보여줄 때, 어떻게 UI를 보여줄지 생각
위 두 가지 사항에 대해, 화면 내 어느 위치에 UI를 배치하고 구현할지 생각
Player가 분신을 만들었을 때, 어떻게 UI를 보여줄지 생각
게임을 클리어 했을 때, 어떻게 UI를 보여줄지 생각
결과 데이터 저장
일단 생각나는 것만 해도 이 정도. 에셋 찾아와서 UI를 꾸미는 게 정말 시간이 오래 걸릴 것 같은데 다른 것도 만만치 않다.
2. 오늘 학습에 대해
Unity의 생명주기때문에 코드가 꼬이는 일이 종종 있다. 매번 난감해서 생소한 코딩을 하며 해결을 하였는데, 오늘은 여러 스크립트가 꼬여 그렇게도 할 수가 없어, UI의 Start() 메서드에 사용하던 모든 코드를 통째로 다른곳으로 옮겨 작업을 해야만했다. 오브젝트를 만들고 컴포넌트를 붙여주는 작업이 그 오브젝트의 Start 메서드보다 빨리 실행되어 Null을 갖게 된 게 이유이다. 조금 더 경험이 쌓여있었다면 더 나은 방법으로 해결 할 수 있었을까 생각한다.
심화주차 팀 과제 2/5일차(발표날 포함)이다. 3D 플랫폼 퍼즐 게임을 만들고 있고, UI 전반 관리의 역할을 맡았다.
1일차에 준비 해 둔 UI요소들
1일차에 UI 요소들을 만들었는데, UI를 관리할 능력이 없어 관련 강의를 듣고 스크립트를 짜는 법을 대강이나마 배워왔다.
어설프지만 그렇게 만들어진 오늘의 스크립트들의 목록은 위와 같다. 아직 매끄럽지 못한 부분이 많아 계속 손보아야 하지만 시간이 부족하다. 뼈대는 어찌저찌 마련하였으니 이젠 원래 스타일대로 깡코딩으로 밀어붙일 차례인 듯 하다.
2. 오늘 학습에 대해
오늘은 코테준비 없이 UI작업을 계속하였다. 메모 해 둘 정도의 트러블슈팅은 없었고, 간단하게 배운 내용을 조금 정리.
게임이 로드 될 때 실행되는 특별한 메서드(유니티에서 지원)
Monobehaviour을 상속받는 Managers.cs에서 사용한 방법이다.
씬 위에 오브젝트와 컴포넌트를 배치하지 않더라고 게임이 로드 될 때 함께 메서드를 실행시켜주는 방법이 있었다.
// 게임이 로드될 때 자동으로 실행
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void OnBeforeSceneLoadRuntimeMethod()
{
Init();
}
게임 시작 시, Awake 실행 이전에 한번만 호출되는 함수이며, 나는 이 메서드에 Init()메서드를 두고, Init 메서드에서 @Managers 오브젝트의 생성과 그 안에 여러가지 Manager류 컴포넌트를 추가하는 방식을 사용하였다.
팝업의 배경에 IPointerClickHandler를 상속
팝업의 외부에 화면을 덮는 이미지를 만들어준 후(투명도는 자유, Raycast Target 옵션을 켜주어야 한다) 사용하면 좋고, 이 배경의 클릭 이벤트에 대응할 메서드를 버튼 컴포넌트 없이 쉽게 작성 할 수 있다.
public class BlockBG : MonoBehaviour, IPointerClickHandler
{
public void OnPointerClick(PointerEventData eventData)
{
// BG 클릭 시, 가장 최근의 팝업을 지우기
TempManagers.UI.ClosePopupUI();
}
}
본인의 경우는 UIManager에서 팝업 관리 메서드 중 하나(닫기)를 사용했지만, 다른 방법으로도 쉽게 팝업을 닫던가 다른 행동을 할 수도 있다.
11시간에 걸친 밤샘 개인 과제 작성을 마쳤다. 오전 10시까지 제출이었는데 기능구현을 마치고 마지막까지 README와 혹시 있을 버그를 열심히 손보았다.
내일부터는 약 일주일간 숙련주차의 팀 과제를 시작한다. 어떤 주제로 하게 될지는 아직 미지수.
어제 캠프내 새로 사귀게 된 분들과 얘기를 하며 구조화에 대한 시야도 넓히고, 강의를 추천받았는데 굉장히 듣고 싶은 내용이었다.
지금까지 내가 소화할 지 미지수인 것도 있고 유료라는 허들이 커서 유료강의는 거의 듣지 않았는데, 이번에 추천받은 Rookiss씨의 강의는 가격이 좀 나가도 내용이 무척 깊어 보여 엄청 듣고싶다고 생각하였다. 로드맵 강의를 따라가면 572,000원.
아, 그리고 이전 입문 주차에 소화하지 못한 강의와, 이번 숙련주차에 소화하지 못한 13/13강 마지막 강의가 있다. 이것도 공부해야 할 듯.
2. 오늘 학습에 대해
과제 도중 이슈
InputField의 값이 갱신되지 않는 버그
발단
public class Withdraw : MonoBehaviour
{
[SerializeField] private int amount;
public InputField amountInput;
public GameObject[] goSuccess;
public GameObject popupObject;
public Text AlertText;
public DisplayText displayTextManager;
public void OnClickButton()
{
if (amount < 1)
amount = int.Parse(amountInput.text);
if (DataManager.Instance.Withdraw(amount))
{
displayTextManager.RenewDisplay();
foreach (GameObject go in goSuccess)
go.SetActive(!go.activeSelf);
return;
}
AlertText.text = "계좌 잔금이 부족합니다.";
popupObject.SetActive(!popupObject.activeSelf);
}
}
값을 필드에 입력하고 출금 버튼을 클릭 시(OnClickButton()), 일정 금액이 출금된다.
다른 금액을 다시 넣어 시도 시, 처음에 넣었던 금액만큼의 처리가 계속되는 현상이 있다.
당장 코드만 보면 크게 문제가 없어 보였으나, 원인을 발견하였다.
코드와 원인에 대해
해당 메서드는, '10000원 버튼', '30000원 버튼', '50000원 버튼'을 누를 경우에도 대응되도록, 미리 설정된 amount가 1만, 3만, 5만이 아닐 경우(amount<1) InputField로부터 유저로부터 입력받은 값을 가져오도록 했다.
그래서, amount가 새로 설정되는 시점부터 그 이후는 amount값이 1보다 크기 때문에 갱신을 하지 않은 것이었다.
해결
조건 분기를 위해 새로운 변수를 추가했다. [SerializeField] private bool useAmountInput;
해당 변수는 에디터에서 체크를 할 수 있으며, InputField를 사용하여 유저가 직접 값을 입력하는 케이스에 체크 해 두도록 한다.
수정된 코드는 아래와 같다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Withdraw : MonoBehaviour
{
[SerializeField] private int amount;
[SerializeField] private bool useAmountInput; // 추가된 부분
public InputField amountInput;
public GameObject[] goSuccess;
public GameObject popupObject;
public Text AlertText;
public DisplayText displayTextManager;
public void OnClickButton()
{
if (useAmountInput) // 수정된 부분
amount = int.Parse(amountInput.text);
if (DataManager.Instance.Withdraw(amount))
{
displayTextManager.RenewDisplay();
foreach (GameObject go in goSuccess)
go.SetActive(!go.activeSelf);
return;
}
AlertText.text = "계좌 잔금이 부족합니다.";
popupObject.SetActive(!popupObject.activeSelf);
}
}
출금에서 발견했지만, 입금도 같은 구조이기 때문에 같이 수정 해 주었다.
유니티의 json 등 파일 쓰기에 대해
실시간으로 세이브 및 로드를 하는 경우, 파일 쓰기를 할 수 있는 경로가 극히 제한된다는 것을 알았다.
Application.persistentDataPath을 데이터 저장 경로로 사용하는 케이스를 종종 볼 수 있다.
나는 프로젝트 파일 근처에서 json파일을 열심히 찾아보다 보이지 않아 구체적인 경로를 검색 해 보았는데,
using System;
using System.Collections.Generic;
public class Solution {
public int[] solution(string[] park, string[] routes) {
int[] answer = new int[2];
// 솔루션 : 주어진 내용을 정말 그대로 수행만 하면 될 것으로 보임
// 0-1. NSWE의 튜플값 마련
// 1. park를 한차례 순회하며(최대 2500회) S의 위치 찾기
// 2. routes의 원소를 foreach순회
// 2-1. 현재자리 기억
// 2-2. for 반복문으로 0-1에서 마련한 튜플값만큼 이동
// 2-2-1. 이동 불가조건 만족 시 원래자리로 복귀
// 3. 현재 자리(index) 반환
// 0-1. NSWE의 튜플값 마련
// 찾아보기. 제네릭으로 튜플값 사용하는 방법
Dictionary<char,(int X,int Y)> going = new Dictionary<char, (int, int)>(){
{'N',(0,-1)}, // (x, y) 순
{'S',(0,1)},
{'W',(-1,0)},
{'E',(1,0)}
};
// 1. park를 한차례 순회하며(최대 2500회) S의 위치 찾기
// 찾아보기. 튜플 변수 선언과 초기화
// 찾아보기. (int, int) index와 같이 사용하는것은 불가능
// (int X, int Y) 로 수정
// 이후 코드에서도 Item1 -> X, Item2 -> Y로 수정
(int X, int Y) index = (-1,-1);
for(int y=0;y<park.Length;y++)
for(int x=0;x<park[0].Length;x++)
if(park[y][x]=='S'){
index = (x,y);
x = park[0].Length;
y = park.Length;
}
// 2. routes의 원소를 foreach순회
foreach(string directionInfo in routes){
// 찾아보기: char로의 형변환
char direction = char.Parse(directionInfo.Split(' ')[0]);
int moveNum = int.Parse(directionInfo.Split(' ')[1]);
// 2-1. 현재자리 기억
// 찾아보기: 튜플값은 값 형식으로 다른 튜플에 할당하면 내용복사
(int X, int Y) preIndex = index;
// 2-2. for 반복문으로 0-1에서 마련한 튜플값만큼 이동
for(int i=0;i<moveNum;i++){
// 찾아보기: 튜플은 아래와 같이 +=는 사용 할 수 없다.
//index += going[direction];
index = (index.X + going[direction].X, index.Y + going[direction].Y);
// 2-2-1. 이동 불가조건 만족 시 원래자리로 복귀
// 찾아보기: 튜플 요소에 접근
if(
index.X<0 ||
index.Y<0 ||
index.X>= park[0].Length ||
index.Y>= park.Length
){
index = preIndex;
break;
}
else if(park[index.Item2][index.Item1] == 'X'){
index = preIndex;
break;
}
// 이동 확인 구문
// Console.WriteLine($"{i}:({index.X}, {index.Y})");
}
}
// 3. 현재 자리(index) 반환
// 아래 구문은 왠지 잘 되지 않음.
// (answer[0], answer[1]) = index;
// 배열의 인덱스 위치에 직접 값을 할당하는 형태는
// 지원이 되는경우도 있고 안되는 경우도 있다는 듯
answer[0] = index.Y;
answer[1] = index.X;
return answer;
}
}
제네릭 형태로 튜플 값을 사용
처음 시도 해 본 내용인데, 이게 되네 싶어서 놀랐다.
Dictionary<char,(int X,int Y)> going = new Dictionary<char, (int, int)>();
튜플 변수
마찬가지로 튜플 변수라는 것도 처음 사용 해 보았다. 배우거나 찾아본 거 없이 적은 구문이 맞는 구문이라 놀랐다..
(int, int) index = ... 형태는 C# 7.0 버전 이상에서 사용할 수 있다고 하고, 프로그래머스의 컴파일러도 이를 만족하지만 어째서인지 되지 않았다. 대신 아래와 같은 형태는 사용 가능했다.
(int X, int Y) index = ... 는 사용 가능. index.X 와 같이 사용 가능
위와 같은 경우 값 참조이기 때문에, (int X, int Y) preIndex = index; 의 경우 값이 복사되며 두 튜플은 별개로 작동한다.
튜플끼리 덧셈 연산자는 사용할 수 없다. index += going[direction];
튜플 내부 변수명으로도 튜플 내의 값을 사용할 수도 있지만 .Item1, .Item2... 로도 사용 가능하다.
한 글자로 된 문자열의 char 로의 형 변환
왠지 사용했던 기억이 없어 익숙하지 않아 찾아보았다.
char direction = char.Parse("C"); 와 같이 char.Parse()를 사용 가능하고, "C"[0]과 같이 사용도 가능하다.