아이템 습득 구현
목표
- 1인칭 시점에 맞는 조작 구현
- 커서 만들기
- 아이템에 커서를 대면 아이템 정보가 나오게 만들기
- 아이템 정보가 뜨고 있을 떄 습득 키(E)를 누르면 "아이템 습득" 로그 찍기
- 아이템 정보를 연동해 인벤토리에 연동하기
1. 1인칭 시점에 맞는 조작 구현하고
'마우스 움직임'으로 회전을 조작하는 방식은 익숙하지 않았다. '이동'관련 코딩은 몇 번 해봤음에도 오래걸렸다.
단순히 목표 오브젝트의 방향대로 힘을 가하거나, 이동을 시키는 것이 아닌 '바라보는 방향' 기준으로 조작해야 한다.
public void Move(Vector2 moveInput)
{
Vector3 forward = cameraTransform.forward;
Vector3 right = cameraTransform.right;
forward.y = 0;
right.y = 0;
forward.Normalize();
right.Normalize();
Vector3 moveDirection = forward * moveInput.y + right * moveInput.x;
Debug.DrawRay(transform.position, moveDirection, Color.red);
rb.velocity = moveDirection * moveSpeed;
}
다음은 '회전' 부분이다. 마우스가 바라보는 위치대로 카메라를 돌려주어야 한다.
public void Look(Vector2 lookInput)
{
float mouseX = lookInput.x * 1 / 5;
float mouseY = lookInput.y * 1 / 5;
// 위 아래 회전은 'X축을 고정한 채로 회전'하는 방향
// 더해서 회전이 -가 되어야 위로 회전하므로 입력을 반대로 해주어야 함
eulerAngleX -= mouseY;
// 양 옆 회전은 'Y축을 고정한 채로 회전'하는 방향
eulerAngleY += mouseX;
eulerAngleX = Mathf.Clamp(eulerAngleX, -90f, 90f);
cameraTransform.rotation = Quaternion.Euler(eulerAngleX, eulerAngleY, 0f);
}
참 간단하지만 단번에 생각나지는 않았다.
분명 다른 방법으로 구현하는 방법도 있을 것인데 나중에 이동 특집으로 찾아봐야겠다.
2. 커서 만들기
처음에는 커서 관련 함수를 사용해 가운데 크로스헤어를 만들고 커서의 위치를 잠궈두면 작동할 줄 알았다.
Cursor.lockState = CursorLockMode.Locked;
Cursor.SetCursor(texture, Vector2.zero, CursorMode.Auto);
다만 생각해보니, 커서의 위치가 우리가 사용하는 InputAction에서의 입력값일텐데 값이 들어가지 않는 사태가 발생하지 않을까 ? 란 생각이 들었다. 다만 찾아보니 마우스 인풋은 '상대적 위치'를 입력하기 때문에 정상적으로 작동한다고 한다.
그리고 보통 크로스헤어는 UI로 사용한다.
왜 ? 커서를 Lock해둬도 ESC 버튼을 누르거나 하면 풀리게 되고, 강제로 업데이트문에서 Lock하면 덜덜 떨리는 현상이
발생하기 때문이다.
Cursor.visible = false;
커서를 보이지 않게 해 두고 UI image의 위치를 0,0으로 두어 해결하였다.
3. 아이템에 커서를 대면 아이템 정보가 나오게 만들기
레이케스트를 사용해서 바라보고 있는 물체의 정보를 가져와 커서의 아래에 표시할 것이다.
public void CheckItem()
{
// Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(Camera.main.transform.position,
Camera.main.transform.forward, out hit, 5f))
{
//아무것도 닿지 않으면 무시
if (hit.collider == null) return;
if (hit.transform.tag == "item")
{
ItemPickUp raycastedItem = hit.transform.GetComponent<ItemPickUp>();
if (currentItem == raycastedItem) return;
currentItem = raycastedItem;
itemInfoApper(raycastedItem);
}
//닿은 물체의 tag가 item이 아니면 정보 OFF
else itemInfoDisappaer();
}
// 아무것도 닿지 않으면 정보 OFF
else itemInfoDisappaer();
}
private void itemInfoDisappaer()
{
currentItem = null;
textMesh.gameObject.SetActive(false);
}
private void itemInfoApper(ItemPickUp raycastedItem)
{
textMesh.text = raycastedItem.ItemData.ItemName;
textMesh.gameObject.SetActive(true);
}
다만 지금은 Camera.main의 위치 정보를 계속해서 받아와서 ray를 사용하기 때문에 Expensive가 뜬다.
이를 해결할 방법을 생각해봐야겠다.
당장 떠오르는 방법으로는 커서의 위치를 ScreenToWorldPositon으로 옮겨서 그 앞으로 쏘는 방법이다.
다만 이것도 계속해서 움직이기 때문에 비싸지 않을까 ?
문제 : 왜 유니티에서는 읽기 전용 프로퍼티를 설정하면 동작하지 않는가 ?
textMesh.text = raycastedItem.ItemData.ItemName;
가장 고생했던 부분은 이 부분이다. 분명 public ItemData itemData {get;}으로 하고 인스펙터 창에서 알맞게 넣어주었는데
NullReferenceException 오류가 발생하였다.
다음 코드를 보자
public ItemData itemData { get; }
'읽기 전용 프로퍼티'이고, 값을 초기화 시에만 설정할 수 있다. (예 : 생성자를 통해서 설정해야만 값 존재)
나는 생성을 하고, 이 데이터에 인스펙터 창에서 itemData를 넣어 주어서 당연히 초기화가 된 줄 알았다.
다만 유니티는 '필드'만 직렬화해서 값을 저장하고 불러온다. 그런데 프로퍼티는 유니티가 직렬화하지 않기 때문에
인스펙터 창에서 값을 설정해도 유니티가 기억하지 못한다.
해결하려면 값을 직렬화하기 위해 [SerializeField]를 붙여서 값을 저장해주고, 따로 접근할 방식을 마련해 주어야 한다.
public class ItemPickUp : MonoBehaviour
{
[SerializeField]
private ItemData itemData { get; }
//public ItemData ItemData => itemData;
}
예를 들어 이렇게 코드를 작성하더라도, '필드'만 직렬화하기 때문에 접근할 수 없고 null로 남아 있다.
직렬화 개념 관련해서는 따로 포스팅할 예정이다.
4.아이템 정보가 뜨고 있을 떄 습득 키(E)를 누르면 "아이템 습득" 로그 찍기
public void PickUpItem()
{
if (currentItem != null && canPickUp)
{
Debug.Log($"Picking up {currentItem.ItemData.ItemName}");
Destroy(currentItem.gameObject);
}
}
private void itemInfoDisappaer()
{
currentItem = null;
textMesh.gameObject.SetActive(false);
canPickUp = false;
}
private void itemInfoApper(ItemPickUp raycastedItem)
{
textMesh.text = raycastedItem.ItemData.ItemName;
textMesh.gameObject.SetActive(true);
canPickUp = true;
}
}
이렇게 bool 타입의 canPickUP이라는 변수를 만들어주고, 상태를 바꿔준다.
만일, canPickUp일 때 PickUpItem을 호출하면 아이템을 부수고, 로그를 찍어준다.
그런데 생각해보니, 여기서 PickUpItem에서 bool 체크를 하고 아이템을 부숴버리면 그 전에 아이템의 정보를
인벤토리에 넘길 때 문제가 있을 것 같다. 부수고 넘긴다면 넘겨지지가 않고, 넘기고 PickUpItem을 한다면 bool체크도 하지 않은 채로 아이템을 습득하는 버그가 있을 수 있다.
그래서 조금 바꿔주었다. (간단한 내용이므로 여기선 생략)
5. 아이템 정보 인벤토리 연동
if (playerCursor.canPickUp)
{
Debug.Log(playerCursor.currentItem.ItemData.ItemName);
inventory.AddItemToInventory(playerCursor.currentItem.ItemData);
playerCursor.PickUpItem();
}
아이템 정보를 playerController에서 연동하면 끝 !
여기서 중재자를 만들어서 처리해도 될 것 같지만, 아직은 구조가 간단하니 굳이.. ? 라는 생각이 든다.
현재까지의 완성 모습이다.
디자인 패턴 : 프록시 패턴
https://febelo0524.tistory.com/76
디자인 패턴 : 프록시 패턴 (Proxy Pattern)
Proxy PatternProxy, '대리', '대리인'이라는 뜻으로 일을 대신 시키는 것을 의미OOP에서는 클라이언트가 객체를 직접 사용하는 것이 아닌 중간 프록시(대리인)을 거쳐 사용하는 패턴대상 클래스,객체
febelo0524.tistory.com
디자인 패턴 : 방문자 패턴
https://febelo0524.tistory.com/77
디자인 패턴 : 비지터 패턴 (Visitor Pattern)
Vistor Pattern'방문자'라는 뜻, 방문자는 어떤 장소에 방문했을 때 장소를 변경시키지 않고 정보를 가져옴특정 객체 구조를 변경시키지 않고 정보를 참고하고 싶을때 사용구현 Ivisitor 인터페이스
febelo0524.tistory.com
코딩테스트 : 직사각형 넓이 구하기
2차원 좌표 평면에 변이 축과 평행한 직사각형이 있습니다. 직사각형 네 꼭짓점의 좌표 [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]가 담겨있는 배열 dots가 매개변수로 주어질 때, 직사각형의 넓이를 return 하도록 solution 함수를 완성해보세요.
using System;
public class Solution {
public int solution(int[,] dots)
{
int answer = 0;
int minX = int.MaxValue;
int maxX = int.MinValue;
int minY = int.MaxValue;
int maxY = int.MinValue;
for (int i = 0; i < 4; i++)
{
minX = Math.Min(minX, dots[i, 0]);
maxX = Math.Max(maxX, dots[i, 0]);
minY = Math.Min(minY, dots[i, 1]);
maxY = Math.Max(maxY, dots[i, 1]);
}
int width = maxX - minX;
int height = maxY - minY;
answer = width * height;
return answer;
}
}
일단 답은 이렇다. 답 자체를 구하는 것은 어렵지 않았다.
'직사각형'이라는 정보가 주어져 있어서 점을 네 군데 찍어보고 X좌표와 Y좌표의 특성을 통해 구했다.
다만 처음에 배열 관련에서 조금 혼동이 있었다.
public class Solution {
public int solution(int[,] dots)
{
int answer = 0;
int x1 = dots[0, 0];
int x2 = dots[0, 1];
int x3 = dots[0, 2];
int x4 = dots[0, 3];
int y1 = dots[1, 0];
int y2 = dots[1, 1];
int y3 = dots[1, 2];
int y4 = dots[1, 3];
int minX = Math.Min(Math.Min(x1, x2), Math.Min(x3, x4));
int maxX = Math.Max(Math.Max(x1, x2), Math.Max(x3, x4));
int width = maxX - minX;
int minY = Math.Min(Math.Min(y1, y2), Math.Min(y3, y4));
int maxY = Math.Max(Math.Max(y1, y2), Math.Max(y3, y4));
int height = maxY - minY;
int size = width * height;
return answer = (int)MathF.Abs(size);
}
}
ㅎㅎ.... 이상하게 접근하려 해서 자꾸 오류가 뜨길래 배열에 관해 더 찾아봤다.
x2가 [1,0]이 되어야 한다. 열 / 행 느낌으로 ! 머릿속에 한 번 더 그려봐야지.
다른 사람의 흥미로운 풀이
크게 없었던 것 같다. 오히려 쉬운 문제일수록 논리가 비슷해지는 것 같다.
코딩테스트 : 다항식 더하기
한 개 이상의 항의 합으로 이루어진 식을 다항식이라고 합니다. 다항식을 계산할 때는 동류항끼리 계산해 정리합니다. 덧셈으로 이루어진 다항식 polynomial이 매개변수로 주어질 때, 동류항끼리 더한 결괏값을 문자열로 return 하도록 solution 함수를 완성해보세요. 같은 식이라면 가장 짧은 수식을 return 합니다.
using System;
using System.Collections.Generic;
public class Solution
{
public string solution(string polynomial)
{
string answer = "";
List<string> numbers = new List<string>();
List<string> xnumbers = new List<string>();
int number = 0;
int xnumber = 0;
string[] polynomialsplit = polynomial.Split(" + ");
for (int i = 0; i < polynomialsplit.Length; i++)
{
if (polynomialsplit[i].Contains("x"))
{
//1일때 처리
if (polynomialsplit[i].Length < 2)
{
xnumbers.Add("1");
}
else
{
string tempt =polynomialsplit[i].Replace("x", "");
xnumbers.Add(tempt);
}
}
else
{
numbers.Add(polynomialsplit[i]);
}
}
for (int i = 0; i < numbers.Count; i++)
{
number += int.Parse(numbers[i]);
}
for (int i = 0; i < xnumbers.Count; i++)
{
xnumber += int.Parse(xnumbers[i]);
}
answer = xnumber.ToString() + "x" + " + " + number.ToString();
return answer;
}
}
논리 구조는 단번에 맞았다.
다만 원래 string tempt =polynomialsplit[i].Replace("x", ""); 에서
polynomialsplit[i].Replace("x", ""); 만 사용했었는데 Replace는 교체하는 것이 아니라 바꾼 문자열을 리턴하는 것이었다.
그리고 지금은 x의 앞이 0일 때나, 숫자가 0일 때의 처리가 따로 없다.
if (xnumber == 0)
{
answer = number.ToString();
}
else if (number == 0)
{
answer = xnumber.ToString() + "x";
}
else
answer = xnumber.ToString() + "x" + " + " + number.ToString();
return answer;
라고 했으나 3개 틀렸다. 앞이 1일때 생략하는 것인가 ?
if (number == 0)
{
if (xnumber == 1)
{
answer = "x";
}
else
{
answer = xnumber.ToString() + "x";
}
}
else if (xnumber == 0)
{
answer = number.ToString();
}
else if (xnumber == 1)
{
answer = "x" + " + " + number.ToString();
}
else
{
answer = xnumber.ToString() + "x" + " + " + number.ToString();
}
return answer;
위에서 xnumber ==1를 처리했다고 생각해서 밑에 xnumber ==1 일때를 추가해주지 않아 시간이 좀 오래걸렸다.
다른 사람의 흥미로운 풀이
다들 예외처리를 비슷하게 하셨다.
코딩테스트 : 안전지대
다음 그림과 같이 지뢰가 있는 지역과 지뢰에 인접한 위, 아래, 좌, 우 대각선 칸을 모두 위험지역으로 분류합니다.

지뢰는 2차원 배열 board에 1로 표시되어 있고 board에는 지뢰가 매설 된 지역 1과, 지뢰가 없는 지역 0만 존재합니다.
지뢰가 매설된 지역의 지도 board가 매개변수로 주어질 때, 안전한 지역의 칸 수를 return하도록 solution 함수를 완성해주세요
using System;
public class Solution {
public int solution(int[,] board)
{
int answer = 0;
int n = board.GetLength(0)-1;
for (int row = 0; row <= n; row++)
{
for (int col = 0; col <= n; col++)
{
if (board[row, col] == 1)
{
else if (row == 0)
{
if (board[row, 1] != 1)
{
board[row, 1] = 2;
}
if (board[row + 1, 0] != 1)
{
board[row + 1, 0] = 2;
}
if (board[row + 1, 1] != 1)
{
board[row + 1, 1] = 2;
}
}
else if (row == n)
{
if (board[row, 0] == 1)
{
if (board[row - 1, 0] != 1)
{
board[row - 1, 0] = 2;
}
if (board[row - 1, 0] != 1)
{
board[row - 1, 0] = 2;
}
if (board[row - 1, 1] != 1)
{
board[row - 1, 1] = 2;
}
}
}
}
}
return answer;
}
}
처음에는 이런 방식으로 예외처리를 하나하나 다 해주려고 하다가, 너무 노가다가 될 것 같아서 다른 방식을 생각했다.
혹시 null일때는 아무것도 안하는 방식으로 처리하면 되지 않을까 ?
using System;
public class Solution {
public int solution(int[,] board)
{
int answer = 0;
for (int row = 0; row < board.GetLength(0); row++)
{
for (int col = 0; col < board.GetLength(1); col++)
{
if (board[row, col] == 1)
{
for (int a = -1; a <= 1; a++)
{
for (int b = -1; b <= 1; b++)
{
if (row + a >= 0 && row + a < board.GetLength(0) && col + b >= 0 &&
col + b < board.GetLength(1))
{
if (board[row + a, col + b] != 1 )
{
board[row + a, col + b] = 2;
}
}
}
}
}
}
}
for (int row = 0; row < board.GetLength(0); row++)
{
for (int col = 0; col < board.GetLength(1); col++)
{
if (board[row, col] == 0)
{
answer++;
}
}
}
return answer;
}
}
null 체크는 되지 않았다, 예를 들어 [0,0]번째에서 -1을 해서 접근하려 하면, 배열의 레인지에서 벗어나 '오류'가 뜨게 되는 것이지 그 안에 null 이 있는 것이 아니다. 그래서 애초에 접근 자체를 하지 못하게 하는 방식으로
if (row + a >= 0 && row + a < board.GetLength(0) && col + b >= 0 &&
col + b < board.GetLength(1))
이렇게 예외처리를 해 주었다.
다른 사람의 흥미로운 풀이
이동 방향을 다르게 표시하는 방법도 있다.
int[] dx = { -1, -1, -1, 0, 0, 1, 1, 1 };
int[] dy = { -1, 0, 1, -1, 1, -1, 0, 1 };
for (int i = 0; i < 8; i++) // 8방향 탐색
{
int nx = row + dx[i];
int ny = col + dy[i];
// 배열 범위 체크
if (nx >= 0 && nx < board.GetLength(0) &&
ny >= 0 && ny < board.GetLength(1) &&
board[nx, ny] == 0) // 안전 지역만 표시
{
board[nx, ny] = 2; // 위험 지역으로 표시
}
}
이렇게 8방향 탐색을 통해 코드를 작성하는 방법도 있다.
다만 내 경우에는 내 코드가 더욱 이해가 잘 되었다.
'TIL' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL회고] 53일차 : 일요일은 쉬는날 (0) | 2025.01.13 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL회고] 52일차 : 혼자 공부하기는 즐거워(아님) (0) | 2025.01.12 |
[멋쟁이사자처럼 부트캠프 TIL회고] 49일차 : 디자인 패턴 (0) | 2025.01.09 |
[멋쟁이사자처럼 부트캠프 TIL회고] 48일차 : 오류 오류 오류 (1) | 2025.01.08 |
[멋쟁이사자처럼 부트캠프 TIL회고] 47일차 : 몬스터 상태 (0) | 2025.01.07 |