TIL(2024,08,01)최종 프로젝트 1차구현 하나씩 만들기

김보근·2024년 8월 1일

Unity

목록 보기
57/113
post-thumbnail

TIL: 특정 영역에서 오브젝트 생성 방지하기

배경
게임 개발 중 특정 영역에서 꽃이 생성되지 않도록 하는 기능을 추가할 필요가 생겼습니다. 이 기능은 꽃이 생성되지 않아야 할 영역을 정의하고, 꽃 배치 시 해당 영역을 피하도록 하는 것입니다.

목표
특정 영역을 중심 좌표와 반경으로 정의하여 꽃이 생성되지 않도록 한다.
해당 영역에서 꽃 배치가 시도될 때 이를 피하도록 코드를 수정한다.
작업 내용
특정 영역 정의
특정 영역을 정의하기 위해 중심 좌표와 반경을 이용했습니다. 세 개의 좌표가 주어졌고, 이를 이용해 중심 좌표와 반경을 계산했습니다. 중심 좌표에서 가장 먼 좌표까지의 거리를 반경으로 설정했습니다.

제한 영역 설정
제한 영역을 설정하기 위해 Tuple<Vector3, float> 타입의 리스트를 사용했습니다. 여기서 Vector3는 중심 좌표를, float는 반경을 의미합니다. 이 리스트는 Start 메서드에서 초기화되었습니다.

반경 계산 메서드
주어진 세 좌표를 기반으로 중심 좌표와 반경을 계산하는 CalculateRadius 메서드를 추가했습니다.

제한 영역 확인 메서드
꽃이 배치될 위치가 제한 영역 내에 있는지 확인하는 IsInRestrictedArea 메서드를 추가했습니다. 이 메서드는 주어진 위치가 리스트에 있는 제한 영역 내에 있는지 확인합니다.

꽃 배치 로직 수정
꽃을 배치하는 PlaceFlowers 메서드를 수정하여, 제한 영역 내에 있는 위치는 건너뛰도록 했습니다.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Quaternion = UnityEngine.Quaternion;
using Random = UnityEngine.Random;
using Vector3 = UnityEngine.Vector3;

public interface IRoot
{
    void ApplyIncreaseRate(BigInteger rate);
    BigInteger GetTotalLifeGeneration();
    void Unlock();
    void ApplyTemporaryBoost(BigInteger multiplier, float duration);
}

public class RootBase : MonoBehaviour, IRoot
{
    public Terrain terrain;  // Terrain 오브젝트
    public GameObject flowerPrefab;  // 꽃 프리팹
    public float brushSize = 1f;  // 브러시 크기
    public int flowersPerPosition = 10;  // 각 위치당 심을 꽃의 개수
    public GameObject[] prePlacedFlowers; // 미리 배치된 꽃 배열
    public OfflineRewardAmountSkill offlineRewardAmountSkill; // 오프라인 보상 스킬 참조
    public SkillCoolDownReduction skillCoolDownReduction;

    protected List<Vector3> flowerPositions;  // 꽃 위치 리스트
    private int currentFlowerIndex = 0;  // 꽃 인덱스
    private int currentPrePlacedFlowerIndex = 0; // 미리 배치된 꽃 인덱스

    public int rootLevel = 0; // 초기 레벨을 0으로 설정
    public BigInteger baseLifeGeneration = 1; // 기본 생명력 생성량
    public BigInteger initialUpgradeCost = 20; // 초기 레벨업 비용
    public BigInteger unlockCost = 0; // 해금 비용
    public BigInteger upgradeLifeCost;
    public TextMeshProUGUI rootLevelText;
    public TextMeshProUGUI generationRateText; // 생산률을 나타내는 텍스트 추가
    public TextMeshProUGUI rootUpgradeCostText;
    public Image lockImage; // 해금 이미지
    public TextMeshProUGUI lockText; // 해금 텍스트
    public bool isUnlocked = false; // 잠금 상태를 나타내는 변수 추가

    public int unlockThreshold = 5; // 잠금 해제에 필요한 터치 레벨
    public int requiredOfflineRewardSkillLevel = 1; // 오프라인 보상 스킬 해금 조건 레벨
    public int skillCoolDownReductionLevel = 1; // 스킬쿨다운 아티팩트 스킬 해금 조건 레벨

    public event System.Action OnGenerationRateChanged;

    protected CameraTransition cameraTransition; // CameraTransition 참조 추가
    private BigInteger currentMultiplier; // 현재 적용 중인 배수
    private Coroutine boostCoroutine; // 부스트 코루틴 참조 변수

    public RootDataSO rootDataSO;

    // 특정 영역들을 정의 (중심과 반경으로)
    public List<Tuple<Vector3, float>> restrictedAreas;

    protected virtual void Start()
    {
        // 특정 영역들을 초기화
        restrictedAreas = new List<Tuple<Vector3, float>> {
            new Tuple<Vector3, float>(new Vector3(-38.777f, -2.15f, -9.51f), CalculateRadius(new Vector3(-38.777f, -2.15f, -9.51f),
                                                                                            new Vector3(-37.08f, -0.36f, 4.74f),
                                                                                            new Vector3(-37.08f, -0.36f, -23.21f)))
        };

        flowerPositions = new List<Vector3>();
        CalculateFlowerPositions();

        // RootDataSO를 이용한 초기화
        if (rootDataSO != null)
        {
            unlockThreshold = rootDataSO.unlockThreshold;
            baseLifeGeneration = BigInteger.Parse(rootDataSO.baseLifeGenerationString);
            unlockCost = BigInteger.Parse(rootDataSO.unlockCostString);
            requiredOfflineRewardSkillLevel = rootDataSO.requiredOfflineRewardSkillLevel;
        }

        OnGenerationRateChanged += UpdateUI; // 이벤트 핸들러 추가
        cameraTransition = FindObjectOfType<CameraTransition>(); // CameraTransition 컴포넌트 참조 초기화
        currentMultiplier = 1;
        LifeManager.Instance.RegisterRoot(this);
        UpdateUI();
    }

    protected virtual void GenerateLife()
    {
        if (!isUnlocked || rootLevel == 0) return; // 잠금 해제된 경우에만 생명력 생성
        BigInteger generatedLife = GetTotalLifeGeneration(); // currentMultiplier는 이미 GetTotalLifeGeneration에 반영됨
        InvokeLifeGenerated(generatedLife);
    }

    protected void InvokeLifeGenerated(BigInteger amount)
    {
        // 생명력 생성 이벤트 호출
    }

    public BigInteger CalculateUpgradeCost()
    {
        if (rootLevel == 0)
        {
            return unlockCost;
        }
        else
        {
            return unlockCost * BigInteger.Pow(120, rootLevel) / BigInteger.Pow(100, rootLevel); // 1.2^rootLevel
        }
    }

    public virtual void UpgradeLifeGeneration()
    {
        if (!isUnlocked) return; // 잠금 해제된 경우에만 업그레이드 가능
        rootLevel++;
        if (rootLevel == 1 || rootLevel % 25 == 0)
        {
            ActivateNextPlantObject();
            if (rootLevel % 25 == 0)
            {
                baseLifeGeneration *= 2; // 25레벨마다 기본 생명력 생성량 두 배 증가
            }
        }
        upgradeLifeCost = CalculateUpgradeCost();
        OnGenerationRateChanged?.Invoke();

        AutoObjectManager.Instance.CalculateTotalAutoGeneration();
        UpdateUI();
    }

    public virtual void CalculateFlowerPositions()
    {
        Vector3 terrainCenter = new Vector3(-7.3f, -0.66f, -5f); // 고정된 중심 위치

        float baseRadius = brushSize / 4;  // 첫 번째 꽃의 위치를 위한 기본 반경
        float rootRadius = 0; // 각 Root에 고유한 반경

        int rootIndex = Array.IndexOf(AutoObjectManager.Instance.roots, this);

        if (rootIndex < 4)
        {
            rootRadius = baseRadius * (rootIndex + 1); // 초기 Root는 기본 반경 증가
        }
        else if (rootIndex >= 6)
        {
            rootRadius = baseRadius * 5 + (rootIndex - 6) * baseRadius; // Root7부터는 이전 Root의 반경을 기반으로 증가
        }
        else
        {
            // Root5와 Root6은 반경을 사용하지 않음
            return;
        }

        // 반경에 따라 배치할 위치의 수를 동적으로 설정
        int numPositions = Mathf.CeilToInt(rootRadius * 2);  // 반경에 비례하여 위치 수를 증가시킴

        flowerPositions = new List<Vector3>(); // flowerPositions 리스트 초기화
        HashSet<Vector3> usedPositions = new HashSet<Vector3>(); // 사용된 위치를 저장하는 집합

        for (int i = 0; i < numPositions; i++)
        {
            float angle = Mathf.Deg2Rad * (360f / numPositions * i);  // 각도 계산
            float x = terrainCenter.x + rootRadius * Mathf.Cos(angle);
            float z = terrainCenter.z + rootRadius * Mathf.Sin(angle);
            Vector3 newPosition = new Vector3(x, terrainCenter.y, z);

            // 위치가 이미 사용된 경우 반복
            while (usedPositions.Contains(newPosition) || IsInRestrictedArea(newPosition))
            {
                angle += Mathf.Deg2Rad * (360f / numPositions); // 각도를 변경하여 위치 재계산
                x = terrainCenter.x + rootRadius * Mathf.Cos(angle);
                z = terrainCenter.z + rootRadius * Mathf.Sin(angle);
                newPosition = new Vector3(x, terrainCenter.y, z);
            }

            flowerPositions.Add(newPosition);
            usedPositions.add(newPosition); // 위치 저장
        }
    }

    void PlaceFlowers(List<Vector3> flowerPositions, GameObject flowerPrefab, ref int currentFlowerIndex)
    {
        if (flowerPositions == null || flowerPositions.Count == 0)
        {
            return;
        }

        if (currentFlowerIndex >= flowerPositions.Count)
        {
            currentFlowerIndex = 0;  // 인덱스 초기화
        }

        Vector3 flowerPosition = flowerPositions[currentFlowerIndex];

        for (int i = 0; i < flowersPerPosition; i++)
        {
            // 원형 오프셋 생성
            float angle = Random.Range(0f, 2f * Mathf.PI);
            float radius = Random.Range(0f, brushSize / 6);
            Vector3 randomOffset = new Vector3(radius * Mathf.Cos(angle), 0, radius * Mathf.Sin(angle));
            Vector3 finalPosition = flowerPosition + randomOffset;

            if (IsInRestrictedArea(finalPosition)) // 제한된 영역을 확인
            {
                continue;
            }

            // Terrain 높이 샘플링 및 최종 위치 조정
            float y = terrain.SampleHeight(finalPosition) + terrain.transform.position.y;
            finalPosition.y = y;

            GameObject newFlower = Instantiate(flowerPrefab, finalPosition, Quaternion.identity);
            newFlower.transform.parent = terrain.transform;  // Terrain 오브젝트의 자식으로 설정

            // 경사면에 맞게 회전
            Vector3 terrainNormal = terrain.terrainData.GetInterpolatedNormal(
                (finalPosition.x - terrain.transform.position.x) / terrain.terrainData.size.x,
                (finalPosition.z - terrain.transform.position.z) / terrain.terrainData.size.z
            );
            newFlower.transform.up = terrainNormal;

            newFlower.isStatic = true;  // Static Batching 적용

            // Material에 GPU Instancing 활성화
            foreach (var renderer in newFlower.GetComponentsInChildren<Renderer>())
            {
                if (renderer.sharedMaterial != null)
                {
                    renderer.sharedMaterial.enableInstancing = true;
                }
            }
        }

        currentFlowerIndex++;
        if (currentFlowerIndex >= flowerPositions.Count)
        {
            currentFlowerIndex = 0;  // 인덱스 초기화
        }
    }

    // 특정 위치가 제한된 영역 내에 있는지 확인
    private bool IsInRestrictedArea(Vector3 position)
    {
        foreach (var restrictedArea in restrictedAreas)
        {
            if (Vector3.Distance(position, restrictedArea.Item1) < restrictedArea.Item2)
            {
                return true;
            }
        }
        return false;
    }

    // 두 좌표 사이의 최대 거리를 계산하여 반경을 구함
    private float CalculateRadius(Vector3 center, Vector3 point1, Vector3 point2)
    {
        float distance1 = Vector3.Distance(center, point1);
        float distance2 = Vector3.Distance(center, point2);
        return Mathf.Max(distance1, distance2);
    }

    private void ActivateNextPlantObject()
    {
        if (currentPrePlacedFlowerIndex < prePlacedFlowers.Length)
        {
            prePlacedFlowers[currentPrePlacedFlowerIndex].SetActive(true);
            currentPrePlacedFlowerIndex++;
        }
        else
        {
            PlaceFlowers(flowerPositions, flowerPrefab, ref currentFlowerIndex);
        }
    }

    public virtual void UpdateUI()
    {
        UpdateRootLevelUI(rootLevel, upgradeLifeCost);
        UpdateGenerationRateUI(GetTotalLifeGeneration());
        UpdateUnlockUI();
    }

    public virtual void ApplyIncreaseRate(BigInteger rate)
    {
        baseLifeGeneration = baseLifeGeneration * (1 + rate);
        OnGenerationRateChanged?.Invoke();
        UpdateUI();
    }

    public virtual void UpdateRootLevelUI(int rootLevel, BigInteger upgradeCost)
    {
        if (rootLevelText != null)
        {
            rootLevelText.text = isUnlocked ? $"꽃 레벨: {rootLevel}" : $"꽃 레벨: 0";
        }

        if (rootUpgradeCostText != null)
        {
            rootUpgradeCostText.text = $"강화 비용: {BigIntegerUtils.FormatBigInteger(upgradeCost)} 물";
        }
    }

    public virtual void UpdateGenerationRateUI(BigInteger generationRate)
    {
        if (generationRateText != null)
        {
            generationRateText.text = $"생산률: {BigIntegerUtils.FormatBigInteger(generationRate)} 물/초";

            if (isUnlocked && rootLevel == 0)
            {
                BigInteger levelOneGenerationRate = baseLifeGeneration * BigInteger.Pow(103, 0) / BigInteger.Pow(100, 0);
                generationRateText.text = $"생산률: {BigIntegerUtils.FormatBigInteger(generationRate)} 물/초 \n1레벨 업그레이드시 자동생산: {BigIntegerUtils.FormatBigInteger(levelOneGenerationRate)} 물/초";
            }
            if (!isUnlocked && rootLevel == 0)
            {
                BigInteger levelOneGenerationRate = baseLifeGeneration * BigInteger.Pow(103, 0) / BigInteger.Pow(100, 0);
                generationRateText.text = $"생산률: {BigIntegerUtils.FormatBigInteger(generationRate)} 물/초 \n1레벨 업그레이드시 자동생산: {BigIntegerUtils.FormatBigInteger(levelOneGenerationRate)} 물/초";
            }
        }
    }

    public virtual void UpdateUnlockUI()
    {
        if (!isUnlocked)
        {
            if (lockText != null)
            {
                lockText.gameObject.SetActive(true);
                if (rootDataSO != null)
                {
                    lockText.text = rootDataSO.unlockConditionText.Replace("\\n", "\n");
                }
                else
                {
                    lockText.text = $"잠금 해제 조건: 세계수 레벨 {unlockThreshold}\n꽃 해금 시 배치 가능 동물 수 + 5";
                }
            }
            if (lockImage != null)
            {
                lockImage.gameObject.SetActive(true);
            }
        }
        else
        {
            if (lockText != null)
            {
                lockText.gameObject.SetActive(false);
            }

            if (lockImage != null)
            {
                lockImage.gameObject.SetActive(false);
            }
        }
    }

    public virtual BigInteger GetTotalLifeGeneration()
    {
        if (!isUnlocked || rootLevel == 0) return 0; // 잠금 해제 전이나 레벨이 0일 때는 0
        BigInteger baseGeneration = baseLifeGeneration * BigInteger.Pow(103, rootLevel - 1) / BigInteger.Pow(100, rootLevel - 1); // 1.03^rootLevel-1
        BigInteger totalGeneration = baseGeneration * currentMultiplier; // currentMultiplier를 곱하여 반환
        return totalGeneration;
    }

    public void Unlock()
    {
        isUnlocked = true;
        upgradeLifeCost = CalculateUpgradeCost(); // 업그레이드 비용 업데이트
        OnGenerationRateChanged?.Invoke(); // 잠금 해제 시 이벤트 트리거
        DataManager.Instance.animalGenerateData.AddMaxAnimalCount();
        UpdateUI();
        Debug.Log("Unlocked successfully.");
    }

    private void CheckUnlockCondition()
    {
        if (!isUnlocked && DataManager.Instance.touchData != null
            && DataManager.Instance.touchData.touchIncreaseLevel >= unlockThreshold)
        {
            Unlock(); // 잠금 해제 조건 만족 시 Unlock 호출
        }
    }

    public void ApplyTemporaryBoost(BigInteger multiplier, float duration)
    {
        if (boostCoroutine != null)
        {
            StopCoroutine(boostCoroutine);
        }
        boostCoroutine = StartCoroutine(TemporaryBoost(multiplier, duration));
    }

    private IEnumerator TemporaryBoost(BigInteger multiplier, float duration)
    {
        currentMultiplier = multiplier;
        OnGenerationRateChanged?.Invoke(); // 부스트 시작 시 생산률 업데이트 이벤트 호출
        UpdateUI(); // 부스트 시작 시 UI 업데이트
        yield return new WaitForSeconds(duration);
        currentMultiplier = 1; // 부스트가 끝나면 배수를 초기값으로 되돌림
        OnGenerationRateChanged?.Invoke(); // 생산률 업데이트 이벤트 호출
        UpdateUI(); // 부스트가 끝난 후 UI 업데이트
    }
}

정리

오늘 작업을 통해 특정 영역에서 오브젝트 생성이 안되도록 하는 기능을 구현했습니다. 이를 위해 특정 영역을 중심 좌표와 반경으로 정의하고, 꽃 배치 시 해당 영역을 피하도록 코드를 수정했습니다. 이를 통해 원하는 위치에서만 꽃이 생성되도록 제어할 수 있게 되었습니다.

profile
게임개발자꿈나무

0개의 댓글