오늘 학습 키워드
유니티 숙련 공부
오늘 학습 한 내용을 나만의 언어로 정리하기
유니티 숙련 공부
스카이박스
- 스카이박스를 만드는 두 가지 방법
- 큐브 맵 : 6개의 텍스쳐로 구성됨
- 구체 : 반구 모양의 하나의 텍스쳐로 구성됨
- 동적으로 변화시켜서 낮 / 밤등의 시간대를 바꿀 수도 있음
- 성능에 영향을 미치기 때문에 주의
Rigidbody - ForceMode
- Force : 동일한 힘을 지속적으로 적용함. (ex : 1, 1, 1, 1)
- Acceleration : 가속도를 적용함. 점점 더 값이 커짐
- Impulse : 순간적인 힘을 적용함. (ex : 점프. 순간적으로 땅을 밀치면서 올라감)
- VelocityChange : 변화하는 속도를 적용함. (ex : 걷기 → 달리기. 현재 속도가 바뀜)
Raycast
- Ray : 가상의 광선. 직선의 시작점(origin)과 방향(direction)을 가짐
// 내 위치에서 앞으로 선 쏘기
Ray ray = new Ray(transform.position, transform.forward);
// 카메라 중심에서 선 쏘기
Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0))
// 마우스를 중심으로 선 쏘기
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition)
-
Raycast : Ray(광선)에 맞은 물체가 무엇인지 판단하고 후처리 하는 방식. 맞은게 있으면 true 반환
- Ray, RaycastHit, MaxDistance, LayerMask 등의 옵션이 있음
-
RaycastHit : Raycast에 의해 검출된 객체의 정보가 담겨있음
- RaycastHit.point : 레이캐스팅이 감지된 위치
- RaycastHit.distance : Ray 원점에서 충돌 지점까지의 거리
- RaycastHit.transform : 충돌 객체의 transform에 대한 참조
Input System
- Send Message
- On + “Action Name” 인 함수를 찾아서 호출하는 방식
- Invoke Event
- 버튼에 함수 집어넣었던 것 처럼 Inspector 상에서 Action에 함수를 설정함
- Invoke C sharp Events
- C# 스크립트에서 Invoke Event 과정을 수행함
- 키 입력을 받고 실행 전, 키 입력 받고 실행 완료, 키 입력 해제 등의 구체적인 상황에 따라 별도의 함수를 등록할 수 있음.
Skybox 직접 만들기
- Material 추가
- Shader를 Standard에서 Skybox > Procedural로 변경
- Window > Rendering > Lighting 진입
- Environment > Skybox Material을 방금 만든 마테리얼로 변경
Input Action 설정하기
- 이동 : wasd
- 점프 : space
- 인벤토리 : Tab
- 공격 : 마우스 왼쪽 클릭
- 카메라 회전 : 마우스 이동
- Value - Delta에 Delta[Mouse] 사용
- 아이템 줍기 : E
플레이어 기본 이동 구현
- CharacterManager : 싱글톤, 플레이어 가지고 있음
- Player : 체력이나 상태 등을 가지고 있음
- PlayerController : 조작 관련 처리
CharacterManager + Player(틀만)
- CharacterManager에서 싱글톤 방어 코드를 탄탄하게 해 두었기 때문에,
// CharacterManager.cs
public class CharacterManager : MonoBehaviour
{
private static CharacterManager instance;
public static CharacterManager Instance
{
get
{
if(instance == null)
{
instance = new GameObject("CharacterManager").AddComponent<CharacterManager>();
}
return instance;
}
}
public Player player;
public Player Player
{
get { return player; }
set { player = value; }
}
private void Awake() // 이게 불렸으면 게임 오브젝트로 Inspector에 있다는 것
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
if(instance != this) // 인스턴스가 내가 아니면
{
Destroy(gameObject); // 삭제
}
}
}
}
- 하이어라키 창에 CharacterManager가 없어도 Instance를 불러와서 쓸 수 있음
// Player.cs
public class Player : MonoBehaviour
{
public PlayerController controller;
private void Awake()
{
// 이 부분 때문에 하이어라키 창에 CharacterManager가 없어도 알아서 생성됨
// 왜냐면 Instance를 부르는 부분에서 instance가 null이면 자동으로 생성되기 때문
CharacterManager.Instance.Player = this;
controller = GetComponent<PlayerController>();
}
}
PlayerController
- 마우스 숨기는 법
Cursor.lockState = CursorLockMode.Locked;
- 이동 (wasd) 구현
// Invoke Unity Evnets로 하려면 CallbackContext 있어야 함
public void OnMove(InputAction.CallbackContext context)
{
// context는 입력에 대한 현재 상태를 가지고 있음
// 키가 눌렸을 때 = Started
// 키를 누르고 있을 때 = Performed
if(context.phase == InputActionPhase.Performed)
{
curMovementInput = context.ReadValue<Vector2>();
}
// 키를 떼었을 때 = Canceled
else if(context.phase == InputActionPhase.Canceled)
{
curMovementInput = Vector2.zero;
}
}
void Move()
{
// forward : 앞/뒤 연산용
// w 누르면 받아오는 값이 0, 1
// s 누르면 받아오는 값이 0, -1
// 따라서 앞/뒤 연산할 때에는 curMovementInput에서 y를 써먹어야 함
// right : 좌/우 연산용
// d 누르면 받아오는 값이 1, 0
// a 누르면 받아오는 값이 -1, 0
// 따라서 좌/우 연산할 때에는 curMovementInput에서 x를 써먹어야 함
Vector3 dir = transform.forward * curMovementInput.y
+ transform.right * curMovementInput.x;
dir *= moveSpeed;
dir.y = _rigidbody.velocity.y; // y축 속도는 유지 (왜냐하면 3차원에서 y는 상하를 의미함)
_rigidbody.velocity = dir;
}
- 회전 (마우스 이동) 구현
- 구현하기 전에, Delta 데이터 타입은 어떻게 나오는건지 궁금해서 직접 디버그로 찍어보기로 함
public void OnLook(InputAction.CallbackContext context)
{
mouseDelta = context.ReadValue<Vector2>();
Debug.Log("Mouse Delta: " + mouseDelta);
}
- 결과 : 마우스를 앞-뒤로 움직일 때에는 y가, 좌-우로 움직일 때에는 x가 변화함.
public void OnLook(InputAction.CallbackContext context)
{
mouseDelta = context.ReadValue<Vector2>();
}
void CameraLook()
{
// 변수명이 X Rot인 이유는 축 잘 생각해보면 이유가 나옴
// 카메라가 위아래로 움직이는거는 X축 기준으로 회전한거임
camCurXRot += mouseDelta.y * lookSensitivity;
camCurXRot = Mathf.Clamp(camCurXRot, minXLook, maxXLook);
// camCurXRot에 -를 붙이는 이유
// 마우스를 위로 움직이면 delta.y는 양수임
// 근데 카메라 회전 축에 양수를 넣으면 위가 아니고 아래로 돌아감
// 그래서 반대로 해주는 것
cameraContainer.localEulerAngles = new Vector3(-camCurXRot, 0, 0);
transform.eulerAngles += new Vector3(0, mouseDelta.x * lookSensitivity, 0);
}
- 점프 (Space) 구현
public void OnJump(InputAction.CallbackContext context)
{
// 점프는 눌리고 있는 동안 계속 작동하면 안되고
// 처음 눌렀을 때 작동해야 하기 때문에 Started 사용함
if(context.phase == InputActionPhase.Started && IsGrounded())
{
// 점프는 순간적으로 힘을 넣어주기 때문에 ForceMode.Impulse 사용함
_rigidbody.AddForce(Vector2.up * jumpPower, ForceMode.Impulse);
}
}
bool IsGrounded()
{
// 플레이어의 책상 다리를 만들어준다는 생각으로 Ray 4개를 쏨
Ray[] rays = new Ray[4]
{
// 살짝 위에서 쏘는 이유는, Ground를 인식하지 못하고 통과해버릴까봐 그럼
new Ray(transform.position + (transform.forward * 0.2f) + transform.up * 0.01f, Vector3.down),
new Ray(transform.position + (-transform.forward * 0.2f) + transform.up * 0.01f, Vector3.down),
new Ray(transform.position + (transform.right* 0.2f) + transform.up * 0.01f, Vector3.down),
new Ray(transform.position + (-transform.right* 0.2f) + transform.up * 0.01f, Vector3.down),
};
for(int i = 0; i < rays.Length; i++)
{
// Ray를 짧게 쐈을 때 Ground를 인식하면 땅에 있는 것으로 판단
if (Physics.Raycast(rays[i], 0.1f, groundLayerMask))
{
return true;
}
}
// 4개의 Ray를 모두 쐈는데도 Ground를 인식하지 못한다면
// 공중에 떠 있다고 판단
return false;
}
UI 만들기
- 만들자 마자 Canvans Scaler부터 바로 수정함 (Scale with Screen Size)
체력바 만들기
- 배경, 아이콘, 줄어드는 부분을 추가함
- 이번에는 줄어드는 부분을 Scale.x를 수정하는게 아닌 다른 방식으로 진행함
- Package Manager에서 2D Sprite를 설치함
- 그런 다음 Project 창에서 우클릭 > 2D > Sprite > Square 선택
- 새로 만든 이미지를 줄어드는 부분에 추가
- 설정을 아래와 같이 수정
- 이러면 Fill Amount를 조정했을 때 왼쪽부터 점점 차오르는 것을 볼 수 있음
빈 오브젝트로 묶기
-
Conditions 만들고 Vertical Layout Group 컴포넌트 추가
- Vertical 말고도 Horizontal, Grid Layout Group도 있음. 상황에 따라 골라 쓰기!
-
Conditions 아래에 체력바 넣고 Ctrl+D 해서 총 3개로 만들기
-
여기까지 결과
코드 연결하기
- 컨디션 하나 하나에 붙는 Condition
- 그 모든 컨디션을 관리하는 UICondition
- 플레이어의 컨디션을 실제로 가지고 있는 PlayerCondition
// Condition.cs
void Update()
{
// UI 업데이트
uiBar.fillAmount = GetPercentage();
}
float GetPercentage()
{
return curValue / maxValue;
}
public void Add(float value)
{
// 최대 값을 넘지 않도록 제한
curValue = Mathf.Min(curValue + value, maxValue);
}
public void Substract(float value)
{
// 최소 값보다 작아지지 않도록 제한
curValue = Mathf.Max(curValue - value, 0);
}
// UICondition.cs
void Update()
{
hunger.Substract(hunger.passiveValue * Time.deltaTime);
stamina.Add(stamina.passiveValue * Time.deltaTime);
if (hunger.curValue == 0f)
{
// noHungerHealthDecay : 배고픔 0일 때 체력 주는 수치
health.Substract(noHungerHealthDecay * Time.deltaTime);
}
if (health.curValue == 0f)
{
Die();
}
}
스탠다드 반 강의 (주제 : 직렬화)
직렬화 개념
- 작성한 코드들은 메모리에 산재됨
- 게임을 껐다가 켰을 때에도 데이터가 유지되어야 함.
- 그래서 저장할 필요가 있는 정보들을 싸그리 모아서 한번에 정리하기로 함
- ⇒ 데이터를 일자로 나열시키자
- ⇒ 직렬화!
- 이걸 되돌리는게 역직렬화!
- “사람이 읽을 수 있는 직렬화” 에 관심이 감
- 직렬화는 프로그램의 기반이다!
- Scene도, meta파일도!!!
PlayerPrefs
- PlayerPrefs도 직렬화 방법
- 근데 별로 쓰진 않음
- 보통 setting에다가 쓴 정보는 쓰기도 함 (해킹 당해도 상관 없는거)
CSV
- 콤마랑 엔터로 나뉘는 데이터
- 엔터 : 한 행
- 콤마 : 데이터 하나
- 미연시같은 대화 위주 게임에서 쓰는 경우가 있음
- 또는 다국어화
- 데이터에 콤마 있는 경우 깨짐
- 다른 문자를 넣어놓고 나중에 변환하는 식으로 해야됨
- 예시 코드
// 예시 데이터 생김새
// 챕터, 대사
// 0_0, 안녕하세요
// 0_1, 예시 데이터입니다
Dictionary<string, string> chatData = new();
void Start()
{
// 데이터 불러오기
TextAsset csvData = Resources.Load<TextAsset>("CSVData");
var data = csvData.text.TrimEnd(); // csv 데이터 맨 밑에는 빈 줄이 있음.
Deserialization(data);
Debug.Log(Show(0, 1)); // 출력 : 예시 데이터입니다.
}
public void Deserialization(string originData)
{
// 엔터, 콤마로 구분됨
// 1번 : 엔터로 자름
// 2번 : 콤마로 자름
var rowData = originData.Split('\n'); // 한 줄 한 줄 나옴
for (int i = 1; i < rowData.Length; i++)
{
var data = rowData[i].Split(','); // [0] : 챕터, [1] : 대사
chatData[data[0]] = chatData[1];
}
}
public string Show(int chapter, int phase)
{
string t = $"{chapter}_{phase}";
return chatData[t];
}
- 단점 : 이 데이터가 무슨 데이터인지를 모름…
XML
- 좀 예전에 쓰였었음.
- 루트 노드 - 자식 노드 - 요소(엘리먼트. 실제로 값을 가지고 있는 부분)
- 값을 열었으면 무조건 닫아야 함
<GameData>
<PlayerData>
<nickname>이혜림</nickname>
<lv>5</lv>
</PlayerData>
</GameData>
// 어려운 버전
public void Save(Player p)
{
XmlDocument xmlDoc = new XmlDocument();
// 루트 노드
XmlNode root = xmlDoc.CreateNode(XmlNodeType.Element, "GameData", string.Empty);
xmlDoc.AppendChild(root); // xml에 루트 노드 붙이기
// 자식 노드
XmlNode child = xmlDoc.CreateNode(XmlNodeType.Element, "PlayerData", string.Empty)
root.AppendChild(child);
// 요소
XmlElement nickname = xmlDoc.CreateElement("nickname");
nickname.InnerText = p.nickname;
child.AppendChild(nickname);
XmlElement lv = xmlDoc.CreateElement("lv");
lv.InnerText = p.lv;
child.AppendChild(lv);
XmlElement money = xmlDoc.CreateElement("money");
money.InnerText = p.money;
child.AppendChild(money);
XmlElement hp = xmlDoc.CreateElement("hp");
hp.InnerText = p.hp;
child.AppendChild(hp);
// 저장
xmlDoc.Save("./Assets/Resources/GameData.xml");
// 새로고침
AssetDatabase.Refresh(); // 이거 있으면 빌드 안되니까 주의하셈
}
public void Load()
{
var data = Resources.Load<TextAsset>("GameData");
XmlDocument xmlDocument = new();
xmlDocument.LoadXml(data.text);
XmlNodeList nodes = xmlDocument.SelectNodes("GameData/PlayerData");
XmlNode playerData = nodes[0];
p.nickname = playerData.SelectSingleNode("nickname").InnerText;
p.lv = int.Parse(playerData.SelectSingleNode("lv").InnerText);
p.hp = float.Parse(playerData.SelectSingleNode("hp").InnerText);
p.money = int.Parse(playerData.SelectSingleNode("money").InnerText);
}
// 쉬운 버전
// Player Class 위에 Serializable이 있어야 함
// 보통 저장할 데이터들은 MonoBehaviour 상속 뺌
string FilePath = "./Assets/Resources/GameData.xml";
public void Save(Player p)
{
// 플레이어를 xml 타입으로 알아서 해줌
XmlSerializer ser = new XmlSerializer(typeof(Player));
using (FileStream stream = new FileStream(FilePath, FileMode.Create))
{
ser.Serialize(stream, p);
}
AssetDatabase.Refresh();
}
public void Load()
{
if(File.Exists(FilePath))
{
XmlSerializer ser = new XmlSerializer(typeof(Player));
using (FileStream stream = new FileStream(FilePath, FileMode.Open))
{
p = ser.Deserialize(stream) as Player;
}
}
}
JSON
- 거의 디폴트가 되었기 때문에 관련 기능이 많음
- 자주 사용하는 세 가지
- Newtonsoft (느리지만 현업에서 많이 씀. 지원되는 기능이 많아서)
- LitJson
- JsonUtility (빠르다! 그냥 이거 써라!! 다만 직렬화 규칙이 빡셈)
- JsonUtility를 써야 하는 이유
- 응답 속도가 JsonUtility가 제일 빠름.
- 기기/상황별 테스트에서도 JsonUtility가 4배 정도 빠르다.
- JsonUtility는 Unity 회사 자체로 만든거임.
- 유니티 엔진 자체는 C++로 제작되어있음
- JsonUtility도 코드 자체가 C++로 되어있음. 그래서 빠름.
- 커스텀 직렬화를 만들 수 있음
- JsonUtility 단점
- Dictionary 직렬화 안됨
public class Character
{
public UserData userData; // 이렇게 해두는게 저장하기 편함
void Save()
{
// 직렬화
var saveData = JsonUtility.ToJson(userData);
// 저장
// Application.persistentDataPath = 알아서 적절한 상대경로 찾아줌
File.WriteAllText(Application.persistentDataPath + "/UserData.txt", saveData);
Debug.Log(Application.persistentDataPath)
}
void Load()
{
// 불러오기
var loadData = File.ReadAllText(Application.persistentDataPath + "/UserData.txt");
// 역직렬화
userData = JsonUtility.FromJson<UserData>(loadData);
}
}
// 직렬화 되게 만듬
// 직렬화가 되어야 유니티 inspector 창에 뜸.
[System.Serializable]
public class UserData
{
public string nickname;
public int lv;
public int money;
public float hp;
}
- 중요한 점 : 클래스 중첩 저장은 괜찮은데, 무조건 [Serializable]을 해줘야 한다!!!
YAML
- 머신러닝하면 쓰이게 됨. 그거 아니면 자주 보지는 않음
Scriptable Object
- 변경되지 않을 내용을 가져다가 써야됨
- 현재 HP처럼 실시간으로 바뀌는 내용은 넣으면 안됨
- 저장 용도로 쓰면 안됨
- SO는 빌드하는 시점으로 데이터가 돌아감
[CreateAssetMenu(fileName = "SOData", menuName = "SO/Data", order = 1)]
public class SOData : ScriptableObject
{
public string nickname;
public float maxHp;
public int lv;
// 장점 : 밑에 얘네들도 inspector에 뜬다
public Rigidbody rb;
public Colider co;
}
암호화/복호화
- 그냥 직렬화/역직렬화만 해버리면 누구든지 데이터를 조작할 수 있음.
- 그래서 직렬화 → 암호화 → 저장 → 불러오기 → 복호화 → 역직렬화 순으로 가는게 맞음
꿀팁
- Json2csharp 이라는 사이트에 가면 Json만 가지고 클래스를 만들어줌. 굿!
내일 학습 할 것은 무엇인지
유니티 숙련 강의 듣기