본문 바로가기

Starters 부트캠프/B - log

유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 16주차 학습 일지

이번 주는 연휴 때문에 하루 늦게 스타터스가 시작했다.

더 쉬었다는 것에 대해서 행복한 것도 있었지만 한편으로는 프로젝트를 진행하는 데 있어, 더 촉박한 느낌이 들었다.

게다가 새롭게 시작한 포톤이 생각보다 더 어려워서 기능을 하나 구현하는데 너무 오랜 시간이 소요되었다.

결국 만족할만한 정도의 성과를 내지는 못 했지만, 그래도 바뀐 강의 스타일과 그동안 궁금했던 것들을 간담회로 많이 알아갈 수 있어서 좋았던 것들도 많았던 한 주라고 요약할 수 있겠다.

 


 

Starters 16주차 -  '런 시뮬레이션 게임(?) 만들기'

 

바뀐 강의 스타일

이번 주부터 수업에서는 하나의 예시 게임을 보여준 뒤에 그 게임처럼 핵심 기능들을 같이 구현하는 시간을 가졌다.

첫 게임으로는 최근 유행하는 '우마 무스메'를 오마주한 스트리머 '우왁굳'의 팬 게임 '고멤 무스메'와 같은 게임을 만들기로 했다.

 

우왁굳 - 고멤무스메 영상

 

기본적으로 자신의 캐릭터를 성장시켜서 경주를 시키고 엎치락 뒤치락 하는 것을 보는 그런 게임이었다.

 

이 게임에서 우리가 수업에서 다룬 내용은 다음과 같았다.

1. 가장 핵심적으로 캐릭터 각각의 능력치를 정하고 부여하는 것
2. UI를 통해 캐릭터가 생성하고, 생성되면 아래에도 초상화를 보여주는 것
3. 이벤트를 통해서 속도가 느려지거나 빨라지게 하는 것
4. 시네머신 카메라를 이용해 역동적인 효과를 더하는 것

 

캐릭터 능력치 부여

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// CreateAssetMenu : 메뉴에서 에셋으로 생성할 수 있게 해준다.
// ScripableObject : 변화가 없는 동일한 데이터를 공유하여 사용할 경우 메모리 사용량을 줄일 수 있다. + 편리성
[CreateAssetMenu(fileName = "StatusData", menuName = "ScriptableObj/StatusData", order = int.MaxValue)]
public class StatusData : ScriptableObject
{
    public string runnerName;
    public Sprite runnerImage;
    public float startSpeed;
    public float maxSpeed;
    public float acceleration;
}


캐릭터들마다 능력치는 차이가 있겠지만, 기본적으로 능력치의 종류는 같고, 이를 경주 중간에 바꾸거나 할 일은 없다.

그렇기 때문에 우리는 ScriptableObject와 CreateAssetMenu를 이용해서 하나하나의 Status Data를 만들어 사용하기로 했다.

 

한 몬스터의 stat들을 한 번에 저장

 

캐릭터 및 UI 생성 

// 캐릭터 생성에 사용할 캐릭터 enum
public enum RunnerType
{
    runner01 = 0,
    runner02,
    runner03
}

[Header("Input StatusData")]
[SerializeField]
private List<StatusData> statusDatas;	// 스탯들을 리스트로 저장하고, 다른 클래스에서는 get만 하도록 함
public List<StatusData> StatusDatas { get { return statusDatas; } }

[Header("Runner Prefabs & Portraits")]
[SerializeField]
private GameObject[] runnerPrefab;	// 캐릭터 프리팹
public List<GameObject> runnerPortraits;	// 캐릭터 초상화들
public List<GameObject> runnerObject = new List<GameObject>();	// 경주에 참여하는 캐릭터 리스트

[Header("ETC")]
[SerializeField]
private Transform spawnPos;		// 생성 위치
public float distanceZ = 2f;	// 캐릭터 간 간격
[SerializeField]
public bool[] runnerIndexArray;	// 캐릭터가 생성되었는지 확인하는 배열

void Awake()
{
    // 스탯 개수만큼 캐릭터를 생성하고, 카메라 설정을 한 뒤에 엑티브를 끔
    for (int i = 0; i < statusDatas.Count; i++)
    {
        var runner = SpawnRunner((RunnerType)i);
        virtualCameras[i].Follow = runner.transform;
        virtualCameras[i].LookAt = runner.transform;
        targetGroup.AddMember(runner.transform, 1, 0);

        runner.SetActive(false);
    }
    runnerIndexArray = new bool[statusDatas.Count];
}

// 러너 타입 enum을 갖고 캐릭터를 생성
public GameObject SpawnRunner(RunnerType type)
{
    var newRunner = Instantiate(runnerPrefab[(int)type], spawnPos.position + new Vector3(0, 0, distanceZ * runnerObject.Count),
                    Quaternion.identity, transform);
    newRunner.GetComponent<MovementController>().StatusData = statusDatas[(int)type];
    newRunner.name = statusDatas[(int)type].runnerName;
    runnerObject.Add(newRunner);
    return newRunner;
}

// 캐릭터를 생성하는 토글 UI 생성
public void ToggleSpawnIndexArray(int index)
{
	// 인덱스 값을 받아서 해당 
    runnerObject[index].SetActive(!runnerObject[index].activeInHierarchy);
    runnerIndexArray[index] = !runnerIndexArray[index];
    runnerPortraits[index].SetActive(!runnerPortraits[index].activeInHierarchy);
}

 

스포너 함수에 위에서 미리 생성한 스탯들을 넣고, 그 개수만큼 캐릭터 생성 토글들을 생성한다.

 

캐릭터 생성 토글들

 

해당 토글들은 각각 인덱스에 해당하는 캐릭터를 실제로 생성 위치에 생성하는 함수를 onValueChanged로 넣었다.

 

public GameObject runnerTogglePrefab;
public RunnerSpawner runnerSpawner;

void Start()
{
    SpawnRunnerToggle();
}

private void SpawnRunnerToggle()
{
    for (int i = 0; i < runnerSpawner.StatusDatas.Count; i++)
    {
        // clozer 문제 때문에 사용 
        int childIndex = i;
        GameObject newRunnerToggle = Instantiate(runnerTogglePrefab, transform);
        newRunnerToggle.transform.GetChild(1).GetComponent<Image>().sprite = runnerSpawner.StatusDatas[i].runnerImage;
        newRunnerToggle.GetComponent<Toggle>().onValueChanged.AddListener
        (
            delegate { runnerSpawner.ToggleSpawnIndexArray(childIndex); }
        );
    }
}

 

참고로 for문 안에서 전역 변수인 i를 그냥 델리게이트의 매개변수로 사용하면, 제대로 적용이 안 되는 경우가 생기는데 이를 클로져 문제라고 배웠다.

이를 방지하고자 i를 다시 childIndex라는 지역 변수로 두어서 이를 활용했다.

 

 

이벤트 구현

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum EventEnum
{
    AllSlow = 0, AllFast, HalfSlow, HalfFast,
    enumLength
}

public enum OneEventEnum
{
    OneSlow = 0, OneFast,
    enumLength
}

public class RaceEvent : MonoBehaviour
{
    public void RepeatRandomEvent()
    {
        EventEnum randomEvent = (EventEnum)Random.Range(0, (int)EventEnum.enumLength);
        Invoke(randomEvent.ToString(), 0f);
        if (GameManager.instance.startRun)
        {
            Invoke("RepeatRandomEvent", 2f);
        }
    }

    // 캐릭터 클래스에서 이벤트 포인터에 트리거 할 시에 자신을 매개변수로 해서 이 함수를 실행
    public void TriggerRandomEvent(GameObject other)
    {
        OneEventEnum randomEvent = (OneEventEnum)Random.Range(0, (int)OneEventEnum.enumLength);
        switch (randomEvent)
        {
            case OneEventEnum.OneSlow:
                OneSlow(other);
                break;
            case OneEventEnum.OneFast:
                OneFast(other);
                break;
        }
    }

    public void AllSlow()
    {
        print("AllSlow");
        foreach (var runner in GameManager.instance.runners)
        {
            runner.GetComponent<MovementController>().currentSpeed *= 0.7f;
        }
    }

    public void AllFast()
    {
        print("AllFast");
        foreach (var runner in GameManager.instance.runners)
        {
            runner.GetComponent<MovementController>().currentSpeed *= 1.5f;
        }
    }

    public void HalfSlow()
    {
        print("HalfSlow");
        List<GameObject> runnersCopy = new List<GameObject>();
        runnersCopy.AddRange(GameManager.instance.runners);
        int halfCount = runnersCopy.Count / 2;
        for (int i = 0; i < halfCount; i++)
        {
            int rnd = Random.Range(0, runnersCopy.Count);
            runnersCopy[rnd].GetComponent<MovementController>().currentSpeed *= 0.7f;
            runnersCopy.Remove(runnersCopy[rnd]);
        }
    }

    public void HalfFast()
    {
        print("HalfFast");
        List<GameObject> runnersCopy = new List<GameObject>();
        runnersCopy.AddRange(GameManager.instance.runners);
        int halfCount = runnersCopy.Count / 2;
        for (int i = 0; i < halfCount; i++)
        {
            int rnd = Random.Range(0, runnersCopy.Count);
            runnersCopy[rnd].GetComponent<MovementController>().currentSpeed *= 1.5f;
            runnersCopy.Remove(runnersCopy[rnd]);
        }
    }

    public void OneSlow(GameObject target)
    {
        print(target.name + "'speed Slow!");
        target.GetComponent<MovementController>().currentSpeed /= 2;
    }

    public void OneFast(GameObject target)
    {
        print(target.name + "'speed Fast!");
        target.GetComponent<MovementController>().FireEffectOn();
        target.GetComponent<MovementController>().currentSpeed *= 2;
    }
}

 

이벤트는 크게 세 가지로 모든 캐릭터에게 적용하는 것과 하나의 캐릭터에 적용하는 것 그리고, 반만 적용하는 것이 있다.

적용하는 효과는 현재 스피드를 2배 혹은 1.5배 하는 버프 효과와 1/2 혹은 0.7배로 줄이는 디버프 효과가 있다.

 

이를 위해 효과들을 각각 enum으로 저장하고, enum 길이만큼 Random.Range를 한 뒤에 그 인덱스에 해당하는 효과 이름과 같은 함수를 실행하게 하였다.

 

 

시네머신 카메라 + 타임라인

 

// 1등을 비추는 카메라의 priority를 조절해서 모든 캐릭터를 비추는 카메라와 번갈아서 보여줌
IEnumerator VcamPriorityControll()
{
    while (startRun)
    {
        vcam.Priority = 9;
        firstLine.gameObject.SetActive(true);
        yield return new WaitForSeconds(10f);
        vcam.Priority = 13;
        firstLine.gameObject.SetActive(false);
        yield return new WaitForSeconds(10f);
    }
}

 

time line으로 시네머신 카메라 엑티브 유무를 조절하는 모습

 

타임라인을 위처럼 배치하면, 게임이 시작되었을 때에 각 캐릭터를 비추는 카메라가 하나씩 액티브 되면서 순차대로 화면에 표시된다.

그리고, 그 위의 코드를 통해서 현재 1등을 비추는 카메라와 전체를 비추는 카메라가 번갈아 나오도록 조절했다.

 

 

결과물 영상

 

게임 플레이 영상

 

 

나만의 게임 만들기 -  'RPG 장르 게임 만들기 + 포톤 현황'

 

RPG 그리고, 멀티 플레이 구현... 

슬픈 이야기를 할 때가 된 것 같다...

내 욕심이 과해서 두 개를 한 번에 하려고 했고, 그 결과 진도가 너무 느려졌다.

포톤의 RPC 개념에 대해 나는 너무 무지했고, 크게 어렵지 않을 기능조차도 동기화 때문에 골머리를 앓았다.

결국 내린 결론은 RPG는 RPG로 포톤은 포톤 따로 공부를 하면서 개발할 계획이다.

특히나 RPG 게임 개발은 나를 포함한 다른 두 분과 같이 스터디처럼 하는 거라서 빠르게 새로운 콘셉트로 개발을 시작해야 할 것 같다.

 

여하튼, 그래서.. 사실 이번 주는 따로 글을 쓸만한 내용이 없다.ㅠ

나 스스로 한테나 이 글을 읽으시는 분들께 모두 사과를 드리며.. 다음 주에는 더 많은 내용으로 채울 수 있도록 노력해보겠다!

파이팅! 나!

 

 

기타-  '간담회 및 다짐'

베이직 코스 - 최종 테스트

간담회 얘기 중 가장 큰 화두는 테스트에 대한 내용이었다.

다음 코스로 가기 위해 피할 수 없는 최종 과정이라는 것이 확실시되었고, 내용이나 난이도 등 많은 것들을 설명해주셨다.

 

12월이라는 것을 감안했을 때에 남은 2 달이라는 시간 열심히 공부한다면, 어렵지 않을 테스트라고 생각이 들었다.

다만, 자만하지 않고, 그전에 배웠던 내용들 중에 제대로 다루지 않았던 내용 위주로 다시 차근차근 공부해야겠다.

 

 

유데미 코리아 바로가기 : 

 

Udemy Korea - 실용적인 온라인 강의, 글로벌 전문가에게 배워보세요. | Udemy Korea

유데미코리아 AI, 파이썬, 리엑트, 자바, 노션, 디자인, UI, UIX, 기획 등 전문가의 온라인 강의를 제공하고 있습니다.

www.udemykorea.com

 

💡 본 포스팅은 유데미-웅진씽크빅 취업 부트캠프 유니티 1기 과정 후기로 작성되었습니다.