Unity learn - Unity Junior Programmer
Mission_주니어 프로그래머: 코드로 창작하기 1
Daily
2024 / 04 | hour | |
09 | 1강 - 플레이어 컨트롤 | 4 |
10 | 1강 - 과제 | 1 |
11 | 2강 - 기본 게임 플레이, 간단한 적과 투사체 | 2 |
12 | 2강 - 마무리 및 과제 | 2 |
13 | 2강 - 실습, 프리미티브를 사용하는 프로젝트 | 5 |
14 | 2강 - 실습 마무리 | 1.5 |
15 | 3강 - 음향 및 효과(배경 반복, 간단한 애니메이션) | 2 |
16 | 3강 - 마무리(파티클 효과) 및 과제, 실습 | 3 |
17 | 4강 - 게임플레이 메카닉(카메라 시점, 파워업과 지속시간) | 3 |
19 | 4강 - 마무리(웨이브), 과제, 5강 - 사용자 인터페이스(점수 Text) | 4 |
20 | 5강 - 마무리(난이도 설정 인터페이스), 과제 | 4 |
21 | 6강 - 프로젝트 문제 조사 및 해결 | 3 |
22 | 6강 - 체크포인트 과제 | 3 |
23 | 씬 플로 및 데이처 관리 (씬 간, 세션 간) | 4 |
24 | 씬 플로 및 데이터 관리 실습 | 6 |
25 | 씬 플로 및 데이터 관리 실습 마무리 | 3.5 |
26 | 객체 지향 원칙 적용 | 6 |
Reference
Tutorial_과정 소개
- 시청자 독려와 천천히 따라오기를 바람
Tutorial_unity 소프트웨어 설치
2021.3 버전은 제시되어 있지 않지만, 2021.3 버전으로 진행할 예정이다.
Tutorial_Project 플레이어 컨트롤
1단원 소개
- 주행 시뮬레이터 프로토타입을 구현하며, 플레이어 컨트롤을 구현하게 된다.
1.1강 - 3D 엔진 시작
- 이전 pathway에서 체험했던 것들을 다시 강의 형식으로 소개간단한 오브젝트 생성 및 배치, 카메라 배치
- ctrl + p 단축키
- 좌상단 기즈모를 통해 x,y, z 뷰를 볼 수 있음
- 에디터 좌상단 select box를 통해, 레이아웃을 변경 할 수 있음
1.2강 - 차량에 동력 만들기
- 스크립트를 생성할 때는, 에디터 내에서 제목을 먼저 정한다. 클래스 네임이 파일명과 같이 생성되기 때문에, 추후 script의 파일명을 수정하면, 충돌을 일으킬 수 있다.
- Update()는 프레임당 실행되는 메서드로, 컴퓨터의 성능에 따라 1초에 실행되는 횟수가 다르다. 1초에 이동하는 거리를 구현하기 위해, 1초를 추적해주는 (The interval in seconds from the lastframe to the current one) Time.deltaTime 를 활용한다.
- transform.Translate(), 해당 위치에서 이동시키기 위한 메서드
- 제공되는 메서드에 대한 설명을 잘 숙지한다.
- Vector3.forward Vector3.left 등 활용할 수 있는 요소도 기억한다.
- 에디터에서 파라미터를 클릭후 좌우로 드래그하여 수치 값을 조정할 수 있다. 플레이 모드에서 적당한 값을 찾을 때 유용하다. 플레이모드에서는 값이 저장되지 않음 자유롭게 수치를 변경하여 확인하되, 저장되지 않음으로 주의한다.
1.3강 - 빠르게 이동하는 물체 추적하기
- main camera 를 차량의 움직임에 맞추어 움직이도록 스크립트를 작성한다.
- FollowPlayer 스크립트 파일을 생성한다.
나하면 FollowCamera 라고 했을텐데, 이런 사소한 부분에서부터 명명법을 잘 정해야겠다. 스크립트의 기능은 플레이어를 따라가는 것 이므로 기능에 맞추어 명명한다.
- public GameObject player 를 멤버 변수로 선언한다. 이렇게 함으로써 에디터에서 차량 오브젝트를 컴포넌트에 입력해줄 수 있다. GameObject에 기본적으로 있는 transform 오브젝트(Transform 클래스)에 접근해, 카메라의 포지션을 정한다. 그리고 적절히 offset을 추가하여 차량을 중심으로 이동시킨다.
- 위 사항을 실시했을 때, 플레이하면 차량의 움직임이 조금씩 튀는 것을 볼 수 있다. 이는 카메라의 위치와 차량의 위치가 Update() 메서드를 통해 동시에 업데이트 되면서, 서로의 순서가 꼬이기 때문이다.
- 위 사항을 방지하기 위해, FollowPlayer 의 Update() 메서드를 LateUpdate() 메서드로 바꿔준다. LateUpdate() 메서드는 Update() 메서드보다 늦게 호출되기 때문에, 위 사항을 방지 해준다.
1.4강 - 주행 준비
- 차량을 방향키로 조작할 수 있도록 수정한다.
- 에디터에서 edit/project settings/Input manager/axes 에서 horizontal 과 vectical을 확인한다. 이미 방향키가 세팅되어 있는 것을 확인할 수 있다.
- 스크립트에서 이 값을 받고, 확인해본다. 우선 두 개의 float 멤버를 선언하고, Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")받아, 이를 각각 작성해둔 transform.Translate() 메서드 인자안에 곱한다.
아마 key : value 형식으로 어딘가 구현되어 있는 모양이다.
- 에디터에서 input을 통해 받은 값은 방향키 입력에 따라 [-1,~1] 범위 값을 가진다.
- 좌우측 이동을 좌우 회전을 하도록 스크립트를 수정한다.
transform.Rotate(회전축, 량(+ 우))
- 프로젝트를 갈무리하면서, 설정할 값을 정한 파라미터(변수)를 스크립트에서 접근지정자를 private오 바꾼다. 이때 GameObject 클래스 오브젝트는 에디터에서 레퍼렌스 되기 때문에 public으로 유지한다.
과제1 - 비행기 프로그래밍
- 주어진 프로젝트에서 문제점을 수정한다.
- Time.deltaTime 숙지, Vector3.forward 등 방향 숙지, transform.Rotate() 숙지, offset 및 LateUpdate() 숙지
- 프로펠러 회전은, transform.Rotate(Vector3.forward, turnSpeed) 를 통해 구현하였다.
실습1 - 프로젝트 설계 문서
- 구현 가능한 요소와 난이도로 첫번째 개인 프로젝트 설계 및 계획을 작성한다.
일단은 패스~
Tutorial_프로젝트 관리 및 팀워크 소개
프로젝트를 성공적으로 완수하기 위한 지침
- 프로젝트 관리와 팀워크를 위한 여러 기술적인 이론과 툴이 존재한다. 이를 위해 커뮤티케이션, 기한 제한 엄수, 요구 사항을 만족하는 목표가 필요하다.
- 프로젝트를 추적할 수 있도록, 문서화가 필요하다. 목적, 대상 목표, 단계, 기한 및 타임라인, 진행도(마일스톤), 우선순위, 책임과 역할
- 시간 관리를 위해 할당 시간 결정, 재검수, 계획 및 툴 통일을 한다
- 커뮤니케이션을 위해 타인의 작업에 영향을 미치는 상황과 문제를 파악한다. 타인의 시간을 존중한다. 비평은 유용하고 구체적이고 존중받을 수 있는 피드백을 제공하는게에 중점을 둔다. 경청한다.
- 프로의식을 갖는다. 시간을 엄수하고, 즉시 응답한다. 타인의 의견과 기여를 인정하고 협업에 적극적으로 참여한다.
설계 문서 및 프로젝트 계획서
- GOD(게임 설계 문서), 타겟 사용자 페르소나, 프로젝트 헌장(규칙, 규범), 기술 사항이 담긴다.
- 설계 문서에서는 대략적인 개요, 특정 파이프라인의 요구 사항 및 기준, 득정 기능의 자세한 설계 사양
- 프로젝트 헌장에는 프로젝트의 당위성, 목표 및 제약, 관계자, 확인된 우험, 이점, 예산에 대한 일반 개요가 담긴다.
- 보조적으로 기숢 문서, 회의록, 제안서 또는 요청서가 반복 작성 된다.
프로젝트 관리 및 진행 상황 추적
- 프로젝트를 추적하기 위해, 단계 파악, 구체적인 프로젝트 역할과 책임을 파악 및 할당, 타임라인 작성, 무분별한 프로젝트 확대 및 과도하게 의욕적인 계획서, 시간 및 여러 자원을 비롯 문제를 파악한다.
Tutorial_Project 기본 게임플레이
2단원 소개
- 플레이어가 제어할 수 없는 장애물이나 적을 만들고, 이에 대항하는 플레이어의 조작요소를 구현한다.
2.1강 - 플레이어 위치 지정
- position 과 if문을 이용한 플레이어 이동반경 제한
2.2강 - 먹이 발사하기
- PlayController에 public GameOject projectilePrefab; 게임 오브젝트 변수 선언, Input.GetKey(KeyCode.Space) 를 사용해 if 문을 작성하고, Instantiate() 메서드 (인수는 주석을 참고한다. )를 사용하여, 오브젝트 생성.
- Input.GetKey(KeyCode.keyCode)는 해당하는 키가 눌리면 True를 반환
- 먹이를 발사할 때마다, 새로운 오브젝트사 생성된다. 이를 제거하는 스크립트를 작성한다. 발사한 먹이가 화면을 벗어나면, 사라지도록 if문과 Destory() 를 사용한다. gaemObject 로 현재 스크립트가 적용되는 오브젝트이다. 비슷하게 동물에게도 적용한다.
2.3강 - 무작위 동물 생성
- GameObject 배열을 선언하면, 크기를 입력하지 않아도, 에디터에서 크기을 넣어줄 수 있다.
- Instaniate() 메서드와 Random.Range() 문을 사용하여, 동물을 랜덤 생성해본다. Random.Range()의 경우 [천번째 수, 두번째 수) 이다.
- Scene vies에서 iso 모드 토글을 해본다. maincamera의 Projection으로 디동해서 Orthographic을 선택해서 확인해 본다.
2.4강 - 충돌 판정
- 우선, 동물이 자동적으로 생성되게 만든다. SpawnMaaeger 스크립트에서 몹 생성 코드을 메서드로 만들어 void SpawnRandomAnimal() 을 선언한다. 그 뒤, Start() 메서드에 InvokeRepeating("SpawnRandomAnimal", 시작딜레이(float), 반족간격(float))를 작성한다. 이 메서드는 해당하는 메서드를 반복 호출한다.
- 충돌판정 구현을 위해, 동물과 먹이에 box collider를 적용하고 collider의 크기를 적절히 수정한 뒤, is trigger을 체크한다. 그 뒤, DetectCollisions 스크립트를 만든 뒤, OnTriggerEnter(Collider other) 함수를 작성하여를 오버라이드 한다
- OnTriggerEnter 함수에 Destroy(gameObject), Destroy(oher.gameObject)를 작성한다. other은 collider에 들어온 오브젝트이다.
과제 2 - 물어 오기 놀이
- 주어진 프로젝트에서 문제점을 수정한다.
- 에디터에서 컴포넌트의 요소, 콜리더의 반경을 확인하고 수정한다.
- 스크립트에서 문제점을 찾아 수정한다.
- 플레이어의 스페이스바 액션에 딜레이를 주기 위해 코드를 작성한다. Time.deltaTime을 누적시켜 기다린 시간을 재고, 이를 딜레이와 함께 if문을 작성하고 초기화하여 구현하였다.
public GameObject dogPrefab;
private bool canFeed = true;
private float feedInterval = 2;
private float feedWatingTime = 2;
// Update is called once per frame
void Update()
{
// spacebar press, send dog
if (canFeed & Input.GetKeyDown(KeyCode.Space))
{
Instantiate(dogPrefab, transform.position, dogPrefab.transform.rotation);
canFeed = false;
}
else if (!canFeed)
{
feedWatingTime += Time.deltaTime;
if (feedWatingTime >= feedInterval)
{
canFeed = true;
feedWatingTime = 0;
}
}
}
public GameObject dogPrefab;
private float feedInterval = 2;
private float feedWatingTime = 2;
void Update()
{
feedWatingTime += Time.deltaTime;
if (Input.GetKeyDown(KeyCode.Space) & feedWatingTime >= feedInterval)
{
Instantiate(dogPrefab, transform.position, dogPrefab.transform.rotation);
feedWatingTime = 0;
}
}
//개선?
public GameObject dogPrefab;
private float feedInterval = 2;
private float feedWatingTime = 2;
void Update()
{
if (feedWatingTime < feedInterval)
{
feedWatingTime += Time.deltaTime;
return;
}
if (Input.GetKeyDown(KeyCode.Space))
{
Instantiate(dogPrefab, transform.position, dogPrefab.transform.rotation);
feedWatingTime = 0;
}
}
- 공 생성 간격 랜덤화는 위의 방법을 수정하여 적용하였다. InVokeRepeating()메서드를 쓰고 실행 함수에 데기시간을 거는 방법도 고려해 볼 만 했지만, 딱히 구현에 옮기지는 않았다.
실습 2 - 프리미티브를 사용하는 새 프로젝트
- 프로젝스 초기, 프리미티브 보브젝트로 구성요소를 간단히 표시한다.
마무리 퀴즈
- Random.Range(float number1, float number2), Randam.Range(int number1, int number2), float 의 경우 [number1,number2], int의 경우 [number1,number2) 인 정수
- private void OnTriggerEnter(colliser other) 메서드에서 other.CompareTag("Spike")를 통해 충돌한 오브젝트를 롹인할 수 있다,
private void OnTriggerEnter(Collider other) {
if(other.CompareTag("Spike")) {
Destroy(other.gameObject);
}
}
큐브 수정
https://play.unity.com/mg/other/webgl-builds-400316
- 큐브가 벽에 계속 부딪치는 것을 구현하고자 했다. cube2 는 초기 운동을 주고 유니티 물리엔진에 맡긴 것이고, cube는 OnTriggerEnter()를 사용하여 물리적이진 않지만 구현했다. cube 1 은 OnCollisionEnter()이 사용되었다
# Rigidbody를 사용한 초기 운동조건과 OnCollisionEnter()메서드는 chatgpt를 통해 작성되었음.
Cube 2 (왼쪽)
Cube (가운데)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cube : MonoBehaviour
{
public MeshRenderer Renderer;
private Vector3 translateAmount;
private Vector3 rotateAmount;
private Material material;
private float[] bColor = { 0f, 0f, 0f, 255f };
private float[] bColorChgs = new float[3];
private bool[] bColorToggles = { true, true, true };
void Start()
{
translateAmount = RandomVector3(Random.Range(80.0f, 120.0f));
rotateAmount = RandomVector3(Random.Range(50.0f, 150.0f));
transform.localScale = Vector3.one * Random.Range(3.0f, 6.0f);
ColorInitial();
}
void Update()
{
transform.Translate(translateAmount * Time.deltaTime, Space.World);
transform.Rotate(rotateAmount * Time.deltaTime);
ChangeColor();
}
private void OnTriggerEnter(Collider other)
{
//평면의 법선벡터
Vector3 planeVector = other.transform.rotation * Vector3.up;
float dot = Vector3.Dot(translateAmount, planeVector);
translateAmount = translateAmount - 2 * dot * planeVector;
dot = Vector3.Dot(rotateAmount, planeVector);
rotateAmount = rotateAmount - 2 * dot * planeVector;
}
}
- 예전에 윈도우 로고가 보호 화면에서 튕겨서 움직이던 것에 착안해 정육면체 안에서 튕키며 움직이는 큐브를 구현하려 했다. 이를 위해, 큐브가 튕겨 반사되는 움직임을 구현하려 했다.
- 평면을 회전한 쿼터니안과 프리미티브 plane의 초기 법선 벡터 (0,1,0)를 dot product 하면, 평면의 법선 벡터가 나온다. 퀴터디안과 xyz벡터의 곱은 * 연산자에 구현되어 있음으로 순서에 주의한다.
- 평면 반사되는 벡터의 공식을 사용해 반사된 (회전이 없을 때의) 큐브의 움직임을 구현하였다.
- 단 회전하는 큐브이 벽과 부딪친 뒤의 움직임을 어떻게 구현해야 할지 몰라서, 그냥 회전 방향만 반사된다고 생각해, 그냥 위의 공식을 적용했다.
- transform.Translate()는 게임 오브젝트의 로컬 좌표계를 따르게 되는데, 이 때문에 translate.Rotate()fh 회전하여 좌표계가 바뀌면 원하는 움직임이 나오질 않았다. 이때 space 인자로 Space.world, 글로벌 좌표계를 넣어 문제를 해결했다.
메서드
Vector3 RandomVector3(float boundry)
{
return new Vector3(Random.Range(-boundry, boundry), Random.Range(-boundry, boundry), Random.Range(-boundry, boundry));
}
void ColorInitial()
{
material = Renderer.material;
for (int i = 0; i < 3; i++)
{
bColor[i] = Random.Range(50f, 200f);
bColorChgs[i] = Random.Range(50.0f, 80.0f);
}
material.color = new Color(bColor[0] / 255f, bColor[1] / 255, bColor[2] / 255, bColor[3] / 255);
}
void ChangeColor()
{
for (int i = 0; i < 3; i++)
{
if (bColorToggles[i])
{
bColor[i] = bColor[i] + bColorChgs[i] * Time.deltaTime;
if (bColor[i] > 255f)
{
bColorToggles[i] = !bColorToggles[i];
}
}
else
{
bColor[i] = bColor[i] - bColorChgs[i] * Time.deltaTime;
if (bColor[i] < 0f)
{
bColorToggles[i] = !bColorToggles[i];
}
}
}
material.color = new Color(bColor[0] / 255f, bColor[1] / 255, bColor[2] / 255, bColor[3] / 255);
}
- 랜덤 벡터 생성, 초기 색상 설정, 색상 변경을 위한 메서드
- Renderer 오브젝트의 Material 오브젝트의 Color 에 접근하여 색상을 수정 할 수 있다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cube2 : MonoBehaviour
{
public MeshRenderer Renderer;
private Rigidbody rb;
private Material material;
private float[] bColor = { 0f, 0f, 0f, 255f };
private float[] bColorChgs = new float[3];
private bool[] bColorToggles = { true, true, true };
void Start()
{
transform.localScale = Vector3.one * Random.Range(3.0f, 6.0f);
rb = GetComponent<Rigidbody>();
rb.velocity = RandomVector3(Random.Range(80.0f, 150.0f));
rb.angularVelocity = RandomVector3(Random.Range(50.0f, 200.0f));
ColorInitial();
}
private void Update()
{
ChangeColor();
}
}
Cube 1 (오른쪽)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cube1 : MonoBehaviour
{
public MeshRenderer Renderer;
private Rigidbody rb;
private Material material;
private float[] bColor = { 0f, 0f, 0f, 255f };
private float[] bColorChgs = new float[3];
private bool[] bColorToggles = { true, true, true };
void Start()
{
transform.localScale = Vector3.one * Random.Range(3.0f, 6.0f);
rb = GetComponent<Rigidbody>();
rb.velocity = RandomVector3(Random.Range(80.0f, 120.0f));
rb.angularVelocity = RandomVector3(Random.Range(50.0f, 150.0f));
ColorInitial();
}
void Update()
{
ChangeColor();
}
void OnCollisionEnter(Collision collision)
{
// 충돌 지점 정보 출력
Debug.Log("is Active");
foreach (ContactPoint contact in collision.contacts)
{
Debug.Log("Contact point: " + contact.point);
Debug.Log("Contact normal: " + contact.normal);
}
// 충돌 시 추가적인 힘 적용 (옵션)
rb.AddForce(-collision.impulse, ForceMode.Impulse);
}
}
- GetComponent<T>(); 를 통해, 현재 게임 오브젝트의 원하는 컴포넌트에 접근할 수 있다. T에 스크립트 이름이자 곧 클래스 이름을 넣어주면 해당하는 컴포넌트를 반환한다. 없을 경우 null이 반환된다. 스크립트 파일과 클래스는 유니티 엔진이 관리해주는 모양인지, 별도로 using으로 임포트하지 않아도, 에디터에서 해당하는 스크립트와 담긴 클래스가 있다면 직접 생성한 스크립트도 접근이 가능했다. public Rigidbody rb; 을 선언해 에디터에서 연결하는 방법도 있디만, OnTriggerEnter(Collider other)과 같이 다른 오브젝트 접근 가능 할때, other.gameObject.GetComponent<T>();를 통해 상호작용한 게임 오브젝트의 컴포넌트도 접근 가능하다. 단 public 만 접근 가능하다.
- Rigidbody 컴포넌트에 접근해여, 게임 오브젝트의 초기 속도와 초기 각속도를 설정해 줄 수 있다.
- collision은 충돌했을 때의 정보를 담아 생기는 인스턴스로, collision을 이용하여 충돌한 다른 게임 오브젝트에 접근 할 수 있다.
- collision.point는 충돌한 좌표를, normal은 충돌면의 법선벡터이다.
- collision.impulse는 충돌한 오브젝트의 충돌량이 담긴 벡터이다. 작용, 반작용이므로, -를 붙여준 것.
- rb.AddForce()를 통해 물체에 힘을 가할 수 있다. ForceMode.Impulse으로 충돌량을, ForceMode.Force로 힘을, ForceMode. Acceleration으로 질량과 관계없이 일정한 가속도를, ForceMode.VelocityChange 로 질량과 관계없이 속도 변경을 줄 수 있다.
Cube 2 (왼쪽)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cube2 : MonoBehaviour
{
public MeshRenderer Renderer;
private Rigidbody rb;
private Material material;
private float[] bColor = { 0f, 0f, 0f, 255f };
private float[] bColorChgs = new float[3];
private bool[] bColorToggles = { true, true, true };
void Start()
{
transform.localScale = Vector3.one * Random.Range(3.0f, 6.0f);
rb = GetComponent<Rigidbody>();
rb.velocity = RandomVector3(Random.Range(80.0f, 150.0f));
rb.angularVelocity = RandomVector3(Random.Range(50.0f, 200.0f));
ColorInitial();
}
private void Update()
{
ChangeColor();
}
}
- 유니티의 게임 엔진을 사용하기 위해서는 Collider에 is trigger이 off 되어 있어야 한다.
Mission_주니어 프로그래머: 코드로 창작하기 2
3단원 소개
- 사운드 및 파티클 효과에 대해
3.1강 - 점프력
- private Rigidbody playerRb를 선언하고, GetComponent<Rigidbody>();를 사용하여 게임 오브젝트의 리지드 바디 컴포넌트를 받는다.
- playerRb.AddForce()를 사용하여 힘을 가한다.
AddForce()에 ForceMode를 넣지 않으면 기본적으로 Force 로 작동한다. AddForce() 는 한 프레임 동안 작동하므로, 지속적인 힘을 구현하려면 프레임마다 동작시켜야한다.
- Physics.gravity를 통해 중력을 바꿀 수 있다.
이때 중력은 게임 전체의 중력이므로 주의, 게임 오브젝트에만 별도의 중력을 가하고 싶다면, use gravity를 off하고, rb.AddForce()를 통해 구현해야한다.
- Input.GetKeyDown(KeyCode.Space)와 조건문을 사용해 점프를 구현한다.
- private bool isOnGround를 사용해 공중 점프를 방지한다. private void OnCollisionEnter(Collider collider);과 점프 조건문에서 bool 값을 토글 시킨다.
- SpawnManage 를 통해 장애물을 반복 생성한다.
3.2 - 월드가 빠르게 지나가도록 구현
- 위 챕터에서 장애물과 배경을 왼쪽으로 움직임으로써 플레이어가 달리는 것처럼 만들었고, 배경이 계속 이어지는 것 처럼 만든다.
- 배경의 위치를 초기화 시킬 것이고, RepeatBackGround 스크립트를 만들어서, transform.position으로 초기 위치를 저장하고, offset과 조건물을 통해, 위치를 초기화시킨다.
- 어색하지 않은 offset을 위해, BackGround 오브젝스에 Box Coillider을 추가하고, GetCompoenet<BoxCollider>()를 통해 boxCollider접근한 뒤, boxCollider.size.x /2 를 offset으로 사용한다.
- 장애물 충돌 시 gameOver를 위해서, 장애물에 Obstacle, 땅에 Ground 태그를 부여한 뒤, PlayerController에 gameOver 불린 변수를 추가하고, OnCollidionEnter(Collision collision) 메서드에 collision.gameObject.CompareTag("태그명")을 사용에 조건문을 작성하고, gameOver을 토글한다.
- gameOver시 배경이동과 장애물 생성을 중단하기 위해, 스크립트간 변수를 접근해야한다(gameOver), GameObject.Find("오브젝트명")을 통해 gameObject에 접근하고 .GetComponent<PlayerController>()를 통해 playerController에 접근한 뒤, gameOver에 접근한다. 이 값과 조건문을 사용해, 이동과 장애물 생성을 제어한다.
3.3 - 생동감 있는 캐릭터 만들기
- 적절한 애니메이션이 있는 상황에서 player 오브젝트에 적절한 애니메이션을 실행시킨다.
- player의 Animator 컴포넌트에서 Contoller를 눌러 설정된 Animator를 확인한다.
- 애니메이션 state를 확인하고, 화살표를 눌러 조건을 확인한다.
- Paremeters를 눌러 파라미터 값들을 확인한다.
애니메이터는 Speed 값이나 여러 파라미터를 자동으로 추적하는 것이 아니라, 해당 파라미터를 설정해 줌으로써, 애니메이션이 작동되는 원리인 것 같다. 이는 animator에서 디폴트로 설정하거나 스크립트로 설정한다.
- parameters의 명명법을 살펴본다. 파라미터명_b, _int, _trig _f 값을 볼 수 있다. 이는 각 값의 데이터형이고, 스크립트에서 데이터 형에 따라 다른 메서드를 사용하니, 파라미터명으로 데이터형을 알 수 있다.
- 달리기 애니메이션을 디폴트로 사용하기 위해, Speed_f를 1로 설정하고, Run_statc state를 Set as Default Layer로 설정한다. 또, 배경 움직임과 어색하지 않게, 애니메이션 재생속도(Speed)를 재설정 한다.
Run_static으로 고정시키기 위해, 이를 빠져나가는 조건인 Speed_f 를 설정한 것이다. 빠져나가는 화살표를 제거하는 방법도 유효하다.
- 점프 애니메이션과, 사망애니메이션을 사용하기 위해, Layers에서 Jumping, Death를 눌러 조건을 확인한다. 각 에니메이션의 사용하기 위한 조건과 관련된 파라미터는 Jump_trig, Death_b, DeathType_int이다.
- PlayerController에서 Animater를 접근하기위해, Animator형 변수를 선언하고, GetComponent<Animator>();를 통해 해당하는 컴포넌트를 가져온다
- Animator의 파라미터를 변경하기 위해, Setter를 사용하는데, 변경하려는 파라미터의 데이터타입에 따라 SetTrigger(), SetBool(), SetInterger() 메서드를 사용하며, 인수로 ("파라미터 명", 수정할 값)을 전달한다.
3.4강 파티클 및 음향 효과
- 파티클 효과와 충돌, 점프 효과를 추가한다.
- 파티클 오브젝트를 player의 자식으로 넣어주고, 에디터에서 컴포넌트에 연결해기 위해 PlayerController에 public ParticleSystem crashParticle, dirtParticle를 선언한다. .Play()메서드로 재생시킨다.
- Player에 AudioSource 컴포넌트를 추가하고, PlayerController에 private AudioSource playerAudio 및 public AudioClip jumpSound, crashSound 를 선언한다. playerAudio,playOnShot(AudioClip clip, float volumeScale) 를 통해, 해당하는 클립을 재생시킨다.
과제 3 - 풍선, 폭탄, 부울
- 학습 내용을 복습하면서, 문제점을 찾아 고친다.
- 문자열 인수를 쓰는 경우, 오타에 주의한다.
- BoxCollier를 사용해서 오브젝트의 사이즈를 가져올 수 있다.
- 아마도, rb.addForce(Vector3 vector) 는 프레임마다 작동하므로,
Time.deltaTime을 곱해야 할 것 같아 곱했다.
유니티 물리엔진은 Time.deltaTime을 고려하여 적절한 힘을 사용한다.
실습 3 - 플레이어 제어
- 학습한 내용으로 자유롭게 무언가를 구현한다.
패스...
Mission - 주니어 프로그래머: 코드로 창작하기 2
4단원 - 게임플레이 메카닉
- 게임의 규칙 또는 메카닉을 구현한다.
4.1강 - 시점 설정
- 빈 게임오브젝트를 카메라의 회전 중심에 위치시키고, Focal Point 로 명명한다. 카메라를 Focal Point의 하위에 위치시킨다. FocalPoint 와 카메라 시점을 일치시켜 놓았기 때문에, FocalPoint의 로컬좌표게에서 z축이 카메라 시점이 된다.
- PlayerController에서 Focal Point 게임오브젝트를 레퍼런스하고, focalPoint.transform.forward 를 사용해, playerRb.AddForce() 의 방향으로 사용한다. 카메라가 보는 방향으로 플레이어가 이동한다.
4.2강 - 플레이어 따라가기
- 플레이어 위치와 적 위치를 사용해서 적이 플레이어를 향하는 벡터를 만들고, .normalize 를 사용해 정규화하여 방향벡터로 사용하여 플레이어를 따라가는 적을 구현한다.
- 스크립트는 가독성을 위해 정리한다.
4.3강 - 파워업 및 카운트다운
- 플레이어에게 버프를 주는 파워업 아이템을 구현한다.
- 적절한 3d 게임오브젝트를 생성하고, Rigidbody 컴포넌트를 추가하고, is trigger를 체크한다. 태그를 Powerup으로 지정한다. 적도 Enemy로 지정한다.
- playerController에서 bool hasPowerUp = false를 선언하고 OnTriggerEnter()메서드에서 PowerUp태그을 사용한 조건문을 작성하고 hasPowerUp = true로 한다. 파워업이 먹은 뒤 사라지면서도 리소스 반환을 위한 Destroy(other,gameObject)도 잊지 말자. OnCollisionEnter() 메서드에서 Enemy 태그 및 hasPowerUp를 사용한 조건문을 작성하고 collision (enemy 오브젝트)의 rigidbody 컴포넌트를 가져오고, 플레이어에서 적으로 향하는 방향벡터를 만든 뒤, float powerUpStrength 수치 만큼 추가적으로 충돌을 받게한다
collider.GetComponent<>(); collision.gameObject.GetComponent<>();
- powerUp 아이템의 효과 소멸 시간을 위해서, IEnumerator PowerupCountdownRoutine() 메서드를 추가한다.
IEnumerator PowerupCountdownRoutine() {
yield return new WaitForSeconds(7);
hasPowerup = false;
powerUpIndicator.gameObject.SetActive(false);
}
파워업 아이템을 먹은 시점에서 동작하도록 StartCoroutine(PowerupCountdownRoutine(); 를 적절한 위치에 추가한다.
파이썬을 할 때, 잠깐 겪은 경험을 바탕으로 이해하자면, yield는 비동기 방식으로 동작한다. 따라서, 출력을 기다리고 실행하는 동기 방식의 함수와 같이 사용될 수 없다. 따라서 StartCoroutine()를 사용해서 동기 함수와 별도의 환경을 만들어 비동기 동작을 수행하도록 한다. 비동기 함수의 경우 return을 만나더라도 종료되지 않고 별도로 종료시켜줘야 함으로, PowerupCountdownRoutine() 이 return을 만났어도, 그 뒤의 코드도 실행되는 것이다.그리고 종료는 리소스 회수를 위해 반드시 넣어줘야 한다. 코루틴에 대한 내용은 나중에 다뤄지면 머리가 깨지면서 알아갈테니 자세한건 패스...
4.4강 - For Loop를 사용한 웨이브 설정
- 반복문을 사용해 enemy가 여러체 생성되게 만들고, 조건문을 사용해 enemy가 다 없어지면 다시 생성되게 만든다. FindObjectsOfType<>()를 통해, 생성된 Enemy객체 배열을 가져올 수 있고, .Length를 사용해 현재 존재하는 적 개체 수를 사용 할 수 있다..
과제4 - 축구 스크립팅
- 이전 내용을 복습하며, 프로젝트에서 잘못된, 누락된 내용을 고친다.
- SpawnManagerX에서 Instistate()메서드가 게임 오브젝트를 생성함과 동시에 반환하므로, 이를 이용해 생성하고자 하는 enemy 의 파라미터를 수정할 수 있었다.
GameObject cunnentEnemy = Instantiate(enemyPrefab, GenerateSpawnPosition(), enemyPrefab.transform.rotation);
cunnentEnemy.GetComponent<EnemyX>().speed += 0.5f* waveCount ;
이때, EnemyX 스크립트의 Start() 메서드에서부터 파라미터가 적용되어있음을 확인 할 수 있었다.
실습 4 - 기본 게임플레이
- 여태 배운 내용으로 자유롭게 무언가를 구현한다.
패스...
5단원 - 사용자 인터페이스
- 사용자에게 보여줄 인터페이스를 만든다.
5.1 - 마우스 클릭
- taeget이 올라갔다 내려갔다 할때, 클릭하면 사라지게 한다. target 스크립트에 OnMouseDown() 메서드를 오버라이드하여, 클릭한 target를 제거한다
- 코루틴을 이용해 게임오브젝트를 생성한다.
- rb.addTorque() 를 사용해 물체에 회전을 시킨다.
5.2 - 점수 기록
- 화면에 점수가 나오도록 한다. 우선 계층에서 UI/ TextMeshPro를 생성한다. 생성된 Convas 하위 테스트 오브젝트의 앵커, 사이즈, 크기를 설정한다.
- GameManager 스크립트에서 using TMPro; 임포트를 하고 public TextMeshProUGUI scoreText를 선언한다. 에디터에서 서 컴포넌트에 위에서 생성한 오브젝트를 연결한다. 스크립트에서 scoreText.text 값을 바꾸어서 화면에서 출력되는 문자열을 바꾸어 줄 수 있다. (GameManager는 빈 오브젝트의 컴포넌트)
Manager 도 게임 오브젝트로 생성하고 스크립트를 컴포넌트로 적용한다. 접근용쓰
- GameManager에서 스코어 값을 갱신하는 public 메서드를 작성하고, Target 스크립트에서 public int pointValue를 사용해 오브젝트 종류 별로 점수를 설정하거나, OnMouseDown()에서 오브젝트를 확인하여 점수를 갱신한다.
5.3 - 게임 오버
- 게임 오버 텍스트를 전 챕터의 방법으로 생성하고, Alinement, wrapper를 적절히 선택한다. 비활성화 시켜놓는다. GameManager 스크립트에서 public TextMeshProUGUI gameOverText 를 선언하고, 텍스트를 활성화 시키는 메서드를 작성한다. gameOverText.gameObject로 접근이 가능하며 .SetActive() 역시 가능하다.
- 게임 오버 상태를 적용하기위해, bool isGameActive 선언하고, 코드를 적절히 수정한다.
- Restart 버튼을 구현하기 위해, 계층에서 UI/ Button를 생성하고 적절히 배치한다. Button 하위에 Text 오브젝트가 있다. Button 오브젝트에 Onclink() 이 있다는 점 확인해두자.
- GameManager에서 씬 가져오기와 Button 객체를 사용하기위해, using UnityEngine.SceneManager; using UnityEngine.UI; 임포트한 뒤, 씬을 가져오는 SceneManager.LoadScene() 메서드를 사용해, 현재 씬을 다시 새로 로드하는 public void RestartGame() 메서드를 작성한다.. SceneManager.GetActiveScene().name이 현재씬의 이름이다. 잘 저장한다
- 에디터에서, 아까 확인한 OnClink() 를 본다. GameManager 컴포넌트가 있는 Game Manager 오브젝트를 연결한다. Function에 GameManager가 포함되어있고, RestartGame() 메서드를 선택할 수 있다.
5.4 - 난이도 설정
- Easy, Normal, Hard 버튼과 타이틀 텍스트를 만들고, canvas 하위에 빈 오브젝트를 생성하고 Start Screen으로 명명한다. 이 오브젝트 하위에 만든 버튼과 텍스트를 옮긴다. 후에 StartGame() 메서드에서 startScreen.SetActive(false)를 할 것.
- DifficultyButton 스크립트를 만들고, 난이도 버튼에 적용한다. Button을 선언하고 Button 컴포넌트를 참조한다GetComponent<>() 사용 , SetDifficulty() 메서드를 작성하고, Start()메서드에 button.OnClick.AddListener(SetDifficulty); 를 작성한다.
지난 챕터에서 에디터에서 연결한 OnClick() 를 코드로 연결한 것이다. 스크립트의 private 메서드도 실행 가능하다. 함수명을 변수처럼 사용한 것은 Delegate라고 부른다.
# 2021.3 버전에서, public으로 생성한 메서드를 에디터에서 OnClick()에 연결한 뒤, 메서드를 private로 바꿔도 실행이 되더라 솔직히 에디터에서 의도한 경우는 아닌것 같긴 하다.
- GameManager에서 public void StartGame() 메서드를 작성하고 인자 int difficulty를 추가한다 이를 이용해 게임 난이도에 영향을 주는 변수를 적절히 수정한다.., 게임시작 관련 내용을 옮겨준다. DifficultyButton에서 public difficulty를 선언하고, 에디터에서 버튼별로 설정한다. GameManager 변수를 선언하고 참조한다. SetDifficulty() 메서드에, StartGame() 메서드를 difficulty 인자와 함께 실행시킨다.
과제 5 -음식 맞히기
- OnMouseEnter() 메서드는 마우스 커서가 이동하기만 해도 실행된다.
- 힌트에는 Time.deltaTime를 사용하여 타이머를 구현하라고 했지만, 코루틴을 사용해 구현하였다. 생각해보니 코루틴의 경우 타이머 동작시간도 타이머에 영향이 미치어, 부정확 할 수 있다.
실습 5 - 에셋 교체
- 프리미티브 오브젝트로 간략히 구현한 프로젝트에 3D 에셋을 입혀본다.
패스...
Tutorial - 사용자 피드백 및 테스트 소개
- 사용자의 요구를 얼마나 잘 충족하는지 정성적 및 정량적 정보를 수집한다.
- 프로젝트 전반에 걸쳐 수반되어야 한다. 미완성 단계이더라도 조기에 자주 테스트 해야한다.
- 목표 정의, 세션 계획, 세션 진행, 결과 평가
- 목표 정의, 프로젝트에 대해 자세히 알고 싶은 내용과 피드백 받기 위해 제공할 내용을 결정 한 뒤, 질문을 준비하고 기간, 규모 등을 정한다.
- 세션 계획, 안내를 하기보다는 관찰에 집중하고, 설명을 하거나 정당화 하지 않는다. 테스트 중에 문제를 수정하지 않는다.
- 세션 진행, 자신의 제품과 자신은 별개라는 점을 기억한다. 모든 피드백의 필요성을 강조한다.
- 세션 평가, 기록을 확인하고 평가한다.
6단원 - 마무리
6.1 - 프로젝트 최적화
- [SerializeField] 에디터에서는 변경하고 싶지만 다른 클래스에서 변경 불가능하게 만들고 싶을때, [SerializeField] 을 쓰면 된다.
20240423 - 직렬화 같은 건가?
- protected, const, readonly (생성 시 변수가 정해지면 변경 불가), static
- FixedUpdate() 물리를 구현할때 유용 Update()보다 선행해서 호출된다. LateUpdate()
- Awake() 오브젝트 존재하면 실행되는 메서드 비활성화된 오브젝트에서도 사용가능
- text.text = 를 통한 텍스트 변경 외에도, .SetText()메서드를 통한 텍스트 변경도 가능하다. 이 경우 글에 색상 등의 효과를 넣는게 가능하다.
자세한건 나중에 경험해 봐야할듯 하다.
- 프로젝트를 import 오브젝트 풀링을 경험해본다.
Prototype 2에서 먹이는 Instatiate()로 생성되는데, Instatiate() 는 객체를 새로만드는 것으로 리소스 소모 어느정도가 있다. 이를 Object Pooling 를 통해, 개선할 수 있는데, 미리 사용할 오브젝트를 어느정도 만들어 놓고, active 를 토글하여 계속 재사용 하는 것이다.
pooler 코드에서 배울 점은, ObjectPooler 클래스 안에, public ObjectPlooer SharedInstance;를 선언 함으로써, 다른 스크립트에서 ObjectPlooer.SharedInstance 로 접근이 용이하게 만들었다.(아마, 게임 상 한개만 존재할 때, 사용할 수 있을 것이다.)
obj.transform.SetParent(this.transform); ( 메서드에 transform 인자가 없음으로, 여기서 this는 가독성을 높이기 위해 사용한 듯)를 통해 에디터 계층구조의 하위로 할 수 있다.
.activeInHierarchy 를 통해 활성화 여부를 확인 할 수 있다.
6.2 - 조사 및 문제해결
- 구글에 찾아보면 다 있더라, 요새는 chatgpt이지만
- rigidbody.AddRelativeForce() 메서드를 사용하면, 로컬좌표계로 힘이 가해진다.
- rigidbody의 .centerOfmass Vector3형 멤버속성를 통해, 무게중심, 힘이 가해지는 지점을 지정할 수 있다
속성과 변수에 대해 chatgpt에 물었다. 변수에는 기초적인 get set 이 있는데, 변수 선언 뒤에 {} ( #마치 자바에서 일회성 인스턴스를 만들 듯)하고 내부에 ger set를 오버라이드 하듯이 별도로 구현할 수 있다. 이는 int 와 같은 변수도 가능하다.인텔리센스에서 get set이 떠있다면 속성이다. 이것으로 set이 가능한 여부와 get이 가능한 여부를 알 수 있다.
- rigidbody.velocity.maganitude을 사용해 속도( float형 )를 알 수 있다.
- Wheel Collider
- collider.isGrounded 콜라이더가 지면에 닿아있는지 확인 할 수 있다. 이를 응용해 차 바퀴가 모두 땅에 닿아있는지 확인할 수 있다.
단, 콜라이더가 다른 콜라이더에 닿아 있다는 것일뿐, 땅이라는 보장은 없다.
6.3 - 프로젝트 공유
- 에디터에서 File / build setting / build를 통해 프로젝트 exe 를 만들 수 있다.,
Toturial - 커리어 조사 및 준비
- 인증시험이 있다더라
- 관심있는 커리어와 역량을 조사한다.
Toturial - 포트폴리오 소개
- 자신의 포트폴리오를 강화해 나간다.
- Behance, ArtStation, Sketchfab (Wix 또는 Squarespace) 대체 github.com itch.io
- Lucidchart, Mindmiester, Google Drawing 메모등 간단 계획
6단원 체크포인트
- 카운팅을 이용한 프로젝트
오브젝트가 떨어지고 박스에 들어가는 개수를 세는 프로젝트를 만든다. OnCoillisionEnter()로 박스에 직접적으로 부딪칠 때, 카운트 되는 방식보단, OnTriggerEnter() 박스 안에 들어갔을 때, 카운트 되도록 박스를 채우는 cube 오브젝트를 생성하고, 공이 통과 하도록 하였다. 추가적으로 박스 위에 쌓이다가 떨어지면 카운트가 떨어지도록 했다.
https://play.unity.com/mg/other/webgl-builds-401700
- 주어진 프로젝트에서 디버그하기
List<> 를 선언하고 new를 통해 할당해야한다. 각 조건문의 의미를 파악하고, 구현하고하 하는 동작에 따라 적절히 코드를 배치한다.
수정 후 스크립트
using System.Collections;
using UnityEngine;
using System.Collections.Generic;
public class CongratScript : MonoBehaviour
{
public TextMesh Text;
public ParticleSystem SparksParticles;
private List<string> textToDisplay = new List<string>();
private float RotatingSpeed;
private float TimeToNextText;
private int currentText;
// Start is called before the first frame update
void Start()
{
TimeToNextText = 0.0f;
currentText = 0;
RotatingSpeed = 1.0f;
textToDisplay.Add("Congratulation");
textToDisplay.Add("All Errors Fixed");
Text.text = textToDisplay[0];
SparksParticles.Play();
}
// Update is called once per frame
void Update()
{
Text.gameObject.transform.Rotate(RotatingSpeed* Time.deltaTime, RotatingSpeed* Time.deltaTime, RotatingSpeed* Time.deltaTime);
TimeToNextText += Time.deltaTime;
if (TimeToNextText > 1.5f)
{
TimeToNextText = 0.0f;
Text.text = textToDisplay[currentText];
currentText++;
if (currentText >= textToDisplay.Count)
{
currentText = 0;
}
}
}
}
Tutorial_실시간 3D 경험 디자인 소개
- 여러 분야가 공동으로 작업하는 만큼, 제작 파이프라인의 관리가 중요하다.
- 여러 디자인 프로세스가 존재한다.
정보처리기사 자격증에서 공부했던 워터폴, 애자일 등이 있다.
Tutorial_버전 관리 설정
- git을 사용한다.
Tutorial_샘플 프로젝트 살펴보기
- 스크립트를 간단히 살펴본다.
Base 스크립트는 Building (다른 스크립트 클래스)를 상속받고, 별다른 내용은 없어보인다. public static Base Instance { get; private set; } getter, setter에 각 각 접근지정자를 적용할 수 있다
Building 스크립트는 추상 클래스로 ///<summary> ///</summary> 로 인텔리센스로 볼수 있는 설명을 추가해 놓았다. [Tooltip("")]으로 간략한 주석 표시 가능, 자원 인벤토리를 가지고 유닛과 상호작용하며, 인벤토리 내의 자원 수정을 다룰 수 있다. 세이브 시스템을 위한 중첩 클래스 [System.Serializable] public class Inventory, 이 클래스의 List로 인벤토리가 구현되었다.
int found = m_Inventory.FindIndex(item => item.ResourceId == resourceId); 람다식 표현을 사용한 간단한 반복 조건문, 컬렉션 변수를 . 으로 레퍼런스하여 사용할 때 람다식 표현을 사용할 수 있다.
virtual 이 선언 된 것을 볼 수 있는데, 이는 메서드, 멤버를 오버라이드한 자식 클래스를 부보 클래스 포인터로 업캐스팅 할때, 오버라이드 된 변수 및 메서드가 참조할 수 있게 만들어 준다.
Unit 스크립트 [RequireComponent(typeof(NavMeshAgent))] 를 통해, 필요한 컴포넌트를 명시 할 수 있다.
추상 클래스
MenuUIHandler 스크립트 클래스 상단에 다음이 있다.
// Sets the script to be executed later than all default scripts
// This is helpful for UI, since other things may need to be initialized before setting the UI
[DefaultExecutionOrder(1000)]
다른 스크립트가 레퍼런스로 초기화가 필요하고, 그 레퍼런스를 사용하게 될 때, 초기화 전 레퍼렌스를 사용하는 것을 방지하기 위한것으로 추정된다. [] 와 관련된 코드는 에디터에서 관리하는듯?
TransporterUnit 스크립트는 Unit 클래스를 상속받았다. 메서드 인자에 ref 사용, 원시 데이터도 주소값, 레퍼렌스하여 전달하는 듯 하다. 객체는 본래 레퍼렌스로 전달하니 객체에는 잘 안 쓰일거 같다. 객체를 ref로 전달하면, 객체를 새로 할당할 때, 해당 변수가 새로 할당된 객체로 바뀐다.
Tutorial_객체 지향 프로그래밍의 원칙
- 추상화 상속 다형성 캡슐화
Tutorial_씬 플로 생성
- 에디터에서, file/Build Settings 에서 Scenes in Build에 Scene를 드래그 앤 드롭하면 추가된다. SceneManager.LoadScene()에서 int 값을 사용 때, idx로 사용됨으로 순서에 주의해둔다.(String 쓰는게 나을 듯?)
- 샘플 프로젝트에서는 UI 이동 스크립트를 Canvas에 적용했다.
- 조건부 컴파일 예시
#if UNITY_EDITOR
EditorApplication.ExitPlaymode();
#else
Application.Quit(); // Unity 플레이어를 종료하는 원본 코드
#endif
Tutorial_씬 간 데이터 지속성 구현
- 데이터 지속성이란, 프로세스보다 데이터를 더 오래 지속시키는 것, 씬이 바뀌면, 기존 씬의 데이터는 손실되는 것이 일반적이다. 다중 세션 경험을 위해선 여러세션에 걸친 저장 및 복원이 이루어져야한다.
- 씬 간 데이터 유지를 위해, MainManager 오브젝트와 스크립트를 생성한다. 인스턴스에 쉽게 접근 가능 하도록, MainManager 클래스 안에 MainManger 정적 변수를 선언한다.
각 스크립트에서 Awake()로 자신의 인스턴스를 만들고 Start()에서 다른 인스턴스를 레퍼런스하는 느낌쓰
- DontDestroyOnLaod()메서드를 사용하여, 씬이 이동되어도 오브젝트가 회수되지 않게 할 수 있다. 이때 해당 씬에 돌아오면 계속 생길 수 있음으로, 싱글톤을 적용한다. 코드는Tutorial_세션 간 데이터 지속성 구현에 있다.
- 해당 정적 오브젝트의 멤버를 수정하여, 씬 이동간 데이터를 유지할 수 있다.
Tutorial_세션 간 데이터 지속성 구현
- 세션 간 데이터 유지를 위해, 데이터의 파일 저장 및 불러오기
- 복잡환 데이터를 저자 가능한 형식으로 전환하는 프로세스를 직렬화 라고 한다. 이를 로드하여 전환한느 것을 역직렬화라고한다.
- Json 예시, JsonUility.ToJson(myData); <==> JsonUtility.From.Json<PlayerData>(json);
[Serializable]
public class PlayerData
{
public int level;
public Vector3 position;
public string playerName;
}
PlayerData myData = new PlayerData();
myData.level = 1;
myData.position = new Vector3(3.0f, 4.4f, 2.3f);
myData.playerName = "John";
-----------------------------------------------------------------------------------------
“level”: 1,
“position”: {
“x” : 3.0,
“y” : 4.4,
“z” : 2.3 },
“playerName”: “John”
너무 좋은 Json, 파이썬에서도 사용했던 Json
불가능한 유형이 있다. 딕셔너리의 경우 직렬화가 가능하지 않기 때문에 저장되지 않는다.
- Application.persistentDataPath 는 운영체제별로 프로그램의 데이터가 지정된 위치에 저장된다. window 운영체제의 경우(필자) C:/Users/park/Appdata/LocalLow/Unity/Junior Programmer PathWay/savefile/json
Junior Programmer PathWay은 설정된 제품명 인듯 하다
- using System.Io, JsonUtility.ToJson() 으로 직렬화 하고 File.WriteAllText("경로", String data) 으로 저장
- File.ReadAllText("경로") 로 불러오고, JsonUtility.FromJson<ClassName>(String json) 로 역직렬화
찾아보면, 전체 텍스트가 아니라 일부 값만 바꾸는 것도 가능할 듯하다. json은 map 형식이니까?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
public class MainManager : MonoBehaviour
{
public static MainManager Instance;
// Start is called before the first frame update
public Color TeamColor;
public void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
LoadColor();
}
public void SaveColor()
{
SaveData data = new SaveData();
data.TeamColor = TeamColor;
string json = JsonUtility.ToJson(data);
File.WriteAllText(Application.persistentDataPath + "/savefile.json", json);
}
public void LoadColor()
{
string path = Application.persistentDataPath + "/savefile.json";
if (File.Exists(path))
{
string json = File.ReadAllText(path);
SaveData data = JsonUtility.FromJson<SaveData>(json);
TeamColor = data.TeamColor;
Debug.Log("file is exist at" + path + data.TeamColor);
}
Debug.Log("file isn't exist at" + path);
}
[System.Serializable]
class SaveData
{
public Color TeamColor;
}
}
과제 - 주어진 프로젝트에서 데이터 지속성 구현
- github에 주어진 프로젝트를 업로드 해본다.
- 프로젝트에 세션 간 데이터 지속성을 구현한다.
- 프로젝트 살펴보기
Brick 게임 오브젝트 별로 다른 점수추가
- using UnityEngine.Events;
- Brick 스크립트에서 public UnityEvent<int> onDestoyed; 를 선언
- MainManager에서 brick오브젝트를 생성하고, 게임 오브젝트마다 리스너 추가, brick.onDestroyed.AddListener(AddPoint); AddPoint는메서드 Void AddPoint(int point); UnityEvent<> 에서 넣어준 데이터 형식을 사용해야하는 듯함, 리스너가 동작하면 AddPoint()에서 점수판 업데이트
- Brick 스크립트에서 OnCollisionEnter()에서 OnDestroyed.Invoke(Int val);
점수별로 다른 컬러, 스크립트에서
- Brick 스크립트에서 renderer 컴포넌트를 가져옴.
- MaterialPropertyBlock block = new MaterialPropertyBlock(); 새로운 머터리얼속성 인스턴스를 생성
- block.SetColor("_BaseColor", Color.green); 생성된 인스턴스에서 색상을 설정하고
- renderer.SetPropertyBlock(block); 게임 오브젝트의 렌더러 컴포넌트에 적용
공이 반사하고 움직이는 것은 전체적으로 물리엔진에 의존함, 스크립트에서는 일정 상황에서 가속 및, 속도 유지 정도.
- 데이터 지속성 추가 계획
목적 - 연습용
주요 기능 - 메뉴 scene 추가, 점수판 기능 추가
세부 사항 - 링크드리스트 사용, 링크드리스트 저장을 위한 List<> 전환, 링크드리스트 사용을 위한 메서드.
https://github.com/park-gihyean/DataPersistence
주로 DataPersistence.cs를 작성함.
주어진 간단한 게임에 메뉴 추가, 유저 이름 받는 기능 추가, 랭크 보드 기능 추가
- 과제 검토.
오랜만에 복습도 하고 코딩도 숙달할 겸, LinkedList 를 직접 구현했다. LinkesList 구현 중, 처음부터 메서드를 크게 만들려 하다보니, 조건문이 덕지덕지 붙게 되고 사소한 오류도 찾는 것이 힘들어졌다. 겱국 코드를 다시 작성했다.
- "메서드를 어떻게 나눠야 할까? " 혹은 "동작을 얼마나 세세하게 나누어야 할까?" 에 대해 해당 메서드가 한 요소 혹은 경우를 보장해야한다. 라고 결론 지었다.
- 빈 노드의 생성을 보장하고, 노드의 위치를 보장하고, 노드의 데이터를 보장한다.
- 초기화 경우을 보장하고, 특수 경우을 보장하고, 일반 경우을 보장한다.
클래스 별 역할 분배는 감을 못 잡겠다. LinkedList 요소가 DataPersistence 안에서만 쓰일거라고 생각해,
inner class로 구현하고 그 외 외부의 접근을 제어했다. 그런 뒤, LinkedList 내의 커서도 그 외부의 접근을 제어했다. 그러고나니 막상, 점수판에 올릴 데이터를 꺼내는 데에 고민이 생겼다. 이외에도 사소한 기능을 추가할 때마다, 코드를 중구난방 추가하고 있었다. 더 깔삼한 방법 없었을까... 여기 구현하는게 맞는건가 싶고, 물론 어느정도는 return 시킬수 있는 데이터등등에 대해 코딩실력과 지식이 부족한 것도 있다.
별도로 주어지는 LinkedList 를 사용하지 않고 직접 구현하려 한것은 연습이였다 하더라고, LinkedList 형식을 생각없이 사용하려한것도 문제일듯 하다. 그러면 솔직히 순위 같은건 데이터 서버를 활용해야 하는 부분이기도 하고, 애초에 계획부터가 생각없었던것 같기도 하고...
만들고 나니 저세상 가버린 가독성...
Toturial - 객체 지향 프로그래밍의 추상화
- 코드를 깔끔하고 단순하게 유지, 실제 필요한 기능만 노출하는 프로세스, 세부 정보를 추상화 하여 중복 코드를 줄이고 가장 유용한 기능에 쉽게 엑세스, SpawnEnemyWave() 라는 높은 수준의 추상화된 메서드를 생성 -> 메서드를 호출하면 적이 생성된다는 더 추상적이 개념만이 중요하다.
- 리펙토링이 용이해져야 한다.
더 높은 수준의 추상화?
가능을 더 높은 수준의 추상화를 구현하기 위해, 구현은 낮은 추상화 수준의 메서드를 사용한다.
다만 조건문의 bool 값도 추상화하는 것에선, 그 정도가 과하면 오히려 가독성이 줄어들 수 있다과 메서드 호출로 인한 오버헤드를 추가적으로 고려해야한다.
- 프로그래머가 코드와 상호 작용하는 방식을 변경하지 않으면서 기능을 개선하는 것
- 리펙토링 프로세스가 간소화 되고, 재사용성이 높아지고, 복잡한 내부 방식이 추상화 된다.
- Tutorial_샘플 프로젝트 기능 살펴보기
UserControl 에서 마우스 클릭으로 유닛을 선택하고 이동하는 것을 구현하기 위해, 카메라의 위치에서 마우스 클릭 위치로 ray를 쏴준다.
public void HandleSelection()
{
var ray = GameCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
//the collider could be children of the unit, so we make sure to check in the parent
var unit = hit.collider.GetComponentInParent<Unit>();
m_Selected = unit;
//check if the hit object have a IUIInfoContent to display in the UI
//if there is none, this will be null, so this will hid the panel if it was displayed
var uiInfo = hit.collider.GetComponentInParent<UIMainScene.IUIInfoContent>();
UIMainScene.Instance.SetNewInfoContent(uiInfo);
}
}
public void HandleAction()
{
//right click give order to the unit
var ray = GameCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
var building = hit.collider.GetComponentInParent<Building>();
if (building != null)
{
m_Selected.GoTo(building);
}
else
{
m_Selected.GoTo(hit.point);
}
}
}
Gamecamera.ScreenPointToRay(Input.mousePosition); GameCamera 는 Camera 클래스 오브젝트, 카메라에서 마우스 클릭 지점으로 선을 쏜다.(추상적인 의미에서.)
Physics.Raycast(ray, out hit) 여기서 hit이 초기화 되는 것 같다. 위에서 쫘준 선과 부딫친 정보 오브젝트가 ray인것 같다.
ray,.collider.GetComponentInParent<Building>() 대상 컴포넌트 중,Building class를 상속받은 컴포넌트를 반환한다.
그 뒤는 구현동작.
Toturial - 객체 지향 프로그래밍의 상속과 다형성
- MonoBehaviour가 없으면 OnTriggerEnter, GetComponent, Start, Update도 사용할 수 없다.
- virtual과 관련된 재정의
여태까지, 메서드 숨김도 오버라이드라고 이해하고 있었다.
- Tutorial_샘플 프로젝트 기능 살펴보기
public abstract class Unit : MonoBehaviour, UIMainScene.IUIInfoContent
{
//
//
//
private void Update()
{
if (m_Target != null)
{
float distance = Vector3.Distance(m_Target.transform.position, transform.position);
if (distance < 2.0f)
{
m_Agent.isStopped = true;
BuildingInRange();
}
}
}
//
//
//
public virtual void GoTo(Building target)
{
m_Target = target;
if (m_Target != null)
{
m_Agent.SetDestination(m_Target.transform.position);
m_Agent.isStopped = false;
}
}
public virtual void GoTo(Vector3 position)
{
//we don't have a target anymore if we order to go to a random point.
m_Target = null;
m_Agent.SetDestination(position);
m_Agent.isStopped = false;
}
//
//
//
protected abstract void BuildingInRange();
}
public class ProductivityUnit : Unit
{
private ResourcePile m_CurrentPile = null;
public float produxtivityMultiplier = 2;
protected override void BuildingInRange()
{
Debug.Log("ProductivityUnit.BuildingInRange() ");
if (m_CurrentPile == null)
{
ResourcePile pile = m_Target as ResourcePile;
if (pile != null)
{
m_CurrentPile = pile;
m_CurrentPile.ProductionSpeed *= produxtivityMultiplier;
Debug.Log("ProductivityUnit" + m_CurrentPile.ProductionSpeed);
}
}
}
void ResetProducivity()
{ if(m_CurrentPile != null)
{
m_CurrentPile.ProductionSpeed /= produxtivityMultiplier;
m_CurrentPile = null;
}
}
public override void GoTo(Building target)
{
ResetProducivity();
base.GoTo(target);
}
public override void GoTo(Vector3 position)
{
ResetProducivity();
base.GoTo(position);
}
}
일정 범위 내 오브젝트에 대해서 반응하도록, 매프레임마다 m_Target 과의 거리를 계산한다.
virtual 선언을 해서 오버라디드 하더라도, base 로 레퍼런스하여, 오버라이드 했던 메서드를 실행가능하다.
virtual 선언 유무의 차이는 메서드 오버라이드 혹은 메서드 숨김이냐 이다. 이는 업캐스팅 할 때에 자식 메서드를 호출할지. 부모메서드를 호출할지의 차이가 생긴다.
구별를 위해서, 오버라이드의 경우 virtual - override 키워드를 사용하고, 메서드 숨김의 경우 자식에 new 키워드를 사용한다.
Toturial - 객체 지향 프로그래밍의 캡슐화
- 캡슐화는 그 코드와 그 코드를 사용하는 다른 코드에 존재하는 기본적인 복잡도 간에 분리 수준을 유지하는 데 중점을 둔다.
- 추상화와 캡슐화를 구별하는 것이 중요하다. 추상화의 핵심은 코드를 요약하고 더 단순하게 만드는 것이고, 캡슐화의 핵심은 액세스 권한을 제어할 수 있도록 값과 데이터를 보호하는 것이다.
- 의도한 대로만 동작할 수 있도록 자신이 생성한 코드를 캡슐화한다.
- 직렬화, 인스펙터에서 변수를 사용 가능하게 만든다.
- {get; set;} 를 사용해 get, set에 다른 접근 지정자를 설정 가능
- {get; set;} 를 사용해, 속성 변수 선언 가능
private float m_ProductionSpeed = 0.5f;
public float ProductionSpeed
{
get { return m_ProductionSpeed; }
set
{
if (value < 0.0f)
{
Debug.LogError("Speed is negative");
}
else
{
m_ProductionSpeed = value;
}
}
}
Toturial - 코드 프로파일링으로 문제 식별
- 코드에 남아있는 병목 현상을 식별하는 방법, 프로파일러는 스크립트의 호출과 메모리 할당, 비주얼 렌더링과 애플리케이션에서 발생하는 일들을 프레임 단위로 정확하게 보여주는 상세 보고서를 생성한다.
- 에디터 상당 Window / Analysis / Profier, 게임 플레이 후 확인
각 요소의 cpu 사용률을 볼 수 있다. 파란 부분이 스크립트가 차지하는 비율
CPU 115.58ms -> 1프레임의 작업이 끝나는 데에 소모된 시간
1s * 1000ms/s / 115.58msperframe = 약 8.62 프레임/초
목표 프레임 60FPS
1000ms/s / 60fps = 16
현재의 밀리초 예산은 목표 밀리초 예산의 7배
PlayerLoop에서 파란색 바, 스크립트가 이 프레임에서 많은 시간을 소모하는 관련 동작을 하고 있다.
102.39ms 동안 2000개의 인스턴스
해당 스크립트의 메서드에서 구획을 더 세부적으로 나눠 프로브 할 수 있다. 예시
Profiler.BeginSample("Handling Time");
HandleTime();
Profiler.EndSample();
Profiler.BeginSample("Handling Time");
Move();
Profiler.EndSample();
profiler에서 다시 파란색 바를 선택하고, TimeLine를 Hierarchy 로 토글한다 무빙이 문제임을 알 수 있다
Tutorial_취업 준비: 주니어 프로그래머
- 쿨럭...
과제 - 자료 제출: 프로그래밍 이론 적용 # 제출 없이 진행
다른 패스웨이 및 코스를 진행할지, 과제를 해보고 진행할지 일단 고민중
https://www.credly.com/badges/34c46ab5-88dd-44ed-873a-8016f9f91f8e/public_url
'Study > Unity learn' 카테고리의 다른 글
Unity / Basic Design Patterns (0) | 2024.05.23 |
---|---|
Unity learn - Make a flag move with shadergraph (0) | 2024.05.03 |
Unity learn - Creative Core (0) | 2024.04.29 |
Unity learn - Unity Essentials (0) | 2024.04.04 |
Unity learn - FPS Microgame Customize (0) | 2024.04.03 |