[Unity][3D-Game] Tower Defense Game (9)

suhan0304·2023년 12월 2일
0
post-thumbnail

강의영상 (11)


개발

Turret 클래스화

통화 시스템 구현에 앞서 Turret을 TurretBlueprint로 바꾸어주자. Turret 프리팹을 바로 가져다 쓰는것이 아니라 별도의 터렛 청사진 클래스를 만들어줘서 해당 클래스에서 터렛과 관련된 정보, 예를 들면 복사에 필요한 프리팹, 가격, 터렛 설명 등, 등을 가져다 쓸 수 있도록 수정한다.

이런 식으로 별도의 클래스로 선언하면 추후에 터렛의 관한 정보 및 콘텐츠를 추가하기도 쉽고 관리하기도 편리하다.

TurretBlueprint.cs

using System.Collections.Generic;
using UnityEngine;

[System.Serializable]

public class TurretBlueprint //MonoBehaviour을 지워줘야 직렬화가 제대로 수행된다.
{
    public GameObject prefab; //터렛 프리팹
    public int cost;          //터렛 건설비용
}

위와 같이 터렛 블루프린트를 만들어놓고 Shop에서 해당 청사진을 이용해 터렛들을 선언해준다.

Shop.cs

public TurretBlueprint standradTurret; //터렛 청사진
public TurretBlueprint MissileLauncher; //미사일 런처 청사진

다음과 같이 각 터렛 청사진 별로 위에서 선언해준 prefab과 cost의 설정이 가능한 것을 확인할 수 있다. 프리팹을 드래그드랍해 초기화해주고 cost 또한 설정해준다.

System.Serializable : 클래스 또는 구조체를 직렬화하는 방법
접근 제한자 "private"를 통해 객체의 정보를 은닉하고 , 캡슐화는 유지하면서, 유니티 에디터의 Inspector에서 값을 변경할 수 있도록 사용하는 키워드이다.

  • 유니티에서는 public을 추천한다.


Shop 수정

이제 Turret Blueprint에 맞게 shop과 buildManager를 수정해보자. Shop의 경우 터렛의 선택만 제어하고 구매 및 건설은 buildManager가 관리하도록 한다.

  • Shop의 PurchaseTurretToBuild를 SelectTurretToBuild로 재작명해준다.

Shop.cs

using UnityEngine;

public class Shop : MonoBehaviour
{
    public TurretBlueprint standradTurret; //터렛 청사진
    public TurretBlueprint MissileLauncher; //미사일 런처 청사진
    BuildManager buildManager;

    private void Start() //빌드 매니저 참조를 위해 instance 초기화
    {
        buildManager = BuildManager.instance;
    }
    public void SelectStandardTurret() //기본 터렛 건설
    {
        Debug.Log("Standard Turret Selected"); //테스트용 출력
        buildManager.SelectTurretToBuild(standradTurret); //기본 터렛을 건설하도록 설정
    }
    public void SelectMissileLauncher() //미사일 런처 건설
    {
        Debug.Log("Missile Launcher Selected"); //테스트용 출력
        buildManager.SelectTurretToBuild(MissileLauncher); //미사일 런처를 건설하도록 설정
    }
}

이제 BuildManager, Node도 모두 수정해준다.

Node.cs

using UnityEngine;
using UnityEngine.EventSystems;

public class Node : MonoBehaviour
{
    public Color hoverColor; //색
    public Vector3 positionOffset;

    [Header("Optional")] //이렇게 Optional을 해놓으면 나중에 봤을때 None으로 되어있어도 놀라지 않음
    public GameObject turret; //public으로 해서 BuildManager에서 나중에 터렛을 설정할 수 있도록 함

    private Renderer rend;
    private Color startColor;

    BuildManager buildManager;

    private void Start()
    {
        rend = GetComponent<Renderer>();//게임이 시작될 때 렌더러를 미리 저장
        startColor = rend.material.color; //시작 색을 저장 후 기억

        buildManager = BuildManager.instance;
    }

    public Vector3 GetBuildPosition()
    {
        return transform.position + positionOffset;
    }

    private void OnMouseDown()
    {
        if (!buildManager.CanBuild) //건설할 터렛이 null이 아니면 True 리턴됨 (BuildManager 참고)
            return;

        if (turret != null) //터렛 오브젝트가 null이 아니면 이미 터렛이 있다는 모습
        {
            Debug.Log("Can't build there! - TODO : Display on screen.");
            return;
        }

        buildManager.BuildTurretOn(this); //이(this) 노드에 Turret을 건설
    }

    private void OnMouseEnter() //마우스가 오브젝트 충돌체에 지나가거나 들어갈 때
    {
        if (EventSystem.current.IsPointerOverGameObject())
            return;

        if (!buildManager.CanBuild) //건설할 터렛이 null이 아니면 True 리턴됨 (BuildManager 참고)
            return;                                 

        //렌더러를 매번 마우스가 들어갈 때마다 아래와 같이 찾는 것은 성능 낭비 -> 게임 시작에서 한 번만 찾고 저장
        //GetComponent<Renderer>().material.color = hoverColor;

        //Start에서 저장된 렌더러를 호출해서 색을 변경
        rend.material.color = hoverColor;
    }

    private void OnMouseExit() //마우스가 오브젝트에서 나갈 때
    {
        rend.material.color = startColor; //startColor로 되돌리기
    }
}

BuildManager.cs

using UnityEngine;

public class BuildManager : MonoBehaviour
{
    public static BuildManager instance; //싱글톤 패턴으로 빌드 매니저를 선언

    private void Awake()
    {
        if (instance != null)  //선언된 적 있으면 더 이상 선언 X
        {
            // 이미 빌드 매니저 인스턴스가 존재
            Debug.LogError("More than one BuildManager in scene!");
            return;
        }
        //게임이 시작하면 새로운 빌드 매니저를 instance에 저장.
        //빌드 매니저는 하나의 인스턴스로만 유지됨 (싱글톤 패턴의 특징 : 하나의 인스턴스만 유지)
        instance = this;
    }

    public GameObject standardTurretPrefab; //기본 터렛 프리팹
    public GameObject missileLauncherPrefab; //미사일 런처 프리팹

    private TurretBlueprint turretToBuild; //노드 선택 시 건설할 터렛
    
    public bool CanBuild { get { return turretToBuild != null; } } // 터렛을 건설할 수 있는지 확인하는 부울 변수 ( Build할 Turret이 Null이 아니면 True 반환 )

    public void BuildTurretOn(Node node)
    {
        GameObject turret = (GameObject)Instantiate(turretToBuild.prefab, node.GetBuildPosition(), Quaternion.identity);
        node.turret = turret; //node의 turret을 turret으로 설정
    }

    public void SelectTurretToBuild (TurretBlueprint turret)
    {
        turretToBuild = turret;
    }
}

이제 리네이밍하면서 떨어진 버튼의 이벤트를 다시 연결해준다.

이제 다시 실행해보면 정상적으로 기존의 Turret을 TurretBlueprint로 수정한 것을 확인할 수 있다.


통화 시스템

이제 통화를 담당할, 플레이어의 상태 정보를 관리할 PlayerStats라는 스크립트를 게임 마스터 안에 추가해준다.

PlayerStats.cs

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

public class PlayerStats : MonoBehaviour
{
    public static int Money; //Static으로 선언하면 메모리 상에 바로 올라가 다른 스크립트에서도 편하게 접근 가능
    public int startMoney = 400; //시작 머니

    void Start()
    {
        Money = startMoney; //게임 시작 시 Money를 시작 머니로 설정    
    }
}

이제 위에서 만든 머니 정보를 가져다가 건설하기 전에 비용과 비교하는 부분을 BuildManager에 추가한다.

BuildManage.cs

public void BuildTurretOn(Node node)
{
    if (PlayerStats.Money < turretToBuild.cost) //플레이어의 돈이 turret의 cost보다 적다면
    {
        Debug.Log("Not Enough Money!"); //돈이 부족하다고 출력 후
        return;                         //건설하지 않고 리턴
    }

    PlayerStats.Money -= turretToBuild.cost; //터렛을 지었으므로 머니를 비용만큼 감소

    GameObject turret = (GameObject)Instantiate(turretToBuild.prefab, node.GetBuildPosition(), Quaternion.identity);
    node.turret = turret; //node의 turret을 turret으로 설정

    Debug.Log("Turret Build! Money Left : " + PlayerStats.Money); 
}

이제 터렛 건설 시 비용만큼 머니가 감소되고 머니가 비용보다 적다면 터렛 건설이 실행되지 않는지 확인해본다.


직렬화 (Serializable)

이번 통화 시스템 구축에서 제일 중요한 점은 기존의 Turret을 TurretBlueprint라는 클래스를 직접 생성한 다음 [System.Serializable]를 통해 직렬화를 시켰으며, TurretBlueprint로 기존의 로직이 그대로 작동하도록 스크립트를 수정하는 과정이 제일 중요하다.

직렬화에 대한 공식 문서를 읽어보는 것도 좋다.

이렇게 직렬화를 하면 해당 필드를 인스펙터 창에서 수정할 수 있고, 프리팹 또는 씬에 저장될 때 유지된다. 이러한 직렬화는 private로 멤버가 선언되어있어도 인스펙터 창에서 수정할 수 있다. (물론 private의 특징은 유지된다.) 이러한 직렬화를 사용하면 객체는 메모리에서 상태를 보존하지 않고도 파일에 저장하거나 네트워크를 전송할 수 있게 된다.

이러한 파일 또는 네트워크로 스트림 통신이 가능해지는 이유는 직렬화를 하게 되면 데이터들을 일렬로 나열된 한 줄의 바이너리 스트림 형태로 파일에 저장이 되기 때문에, 저장 장치로부터 읽고 쓰기가 쉬워지기 때문이다.

  • 진짜 단어 글대로 모든 인스턴스 멤버 변수들의 값을 일렬로 "직렬화"해서 파일로 저장하겠다는 의미이다.

스크립트의 필드가 직렬화되도록 하는 조건이 있는데

  • 첫 번째, public이거나 SerializedField 속성이 있어야한다.
  • 두 번째, static, const, readonly가 아니어야한다.
  • 마지막, fieldtype은 직렬화 할 수 있는 타입 이어야만 직렬화가 가능하다.

Undead Survivor 개발을 진행할 때 몬스터의 소환 데이터를 별도의 클래스로 작성한 후에 직렬화 한 다음 사용했었다. 해당 문서를 참조하자.


결과물

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글