오늘 학습 키워드

유니티 숙련 공부

오늘 학습 한 내용을 나만의 언어로 정리하기

유니티 숙련 공부

스카이박스

  • 스카이박스를 만드는 두 가지 방법
    • 큐브 맵 : 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만 가지고 클래스를 만들어줌. 굿!

내일 학습 할 것은 무엇인지

유니티 숙련 강의 듣기