
1. Vehicle Spawner (장애물 생성)
2. Item Spawner (아이템 생성)
3. 다양한 음식
게임을 더 재밌고 흥미롭게 하는 방해요소를 만들 것이다.
이 게임에서는 방해요소를 차량으로 할 것인데, 도로에서 차량이 나오고 이 차량에 부딧히면, hp가 감소한다거나, 움직이지 못하게 되는 등의 시스템을 생각하고 있다.

이것을 실행하려면 아마 맵과 플레이어의 위치에 관한 코드가 있는 MapManager와 연관되게끔 해야 할 것이다.
public GameObject[] maps = new GameObject[9];
차량을 생성하는 VehicleSpawner에서 생성될 위치를 구하기 위해 이런게 변경하였다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class VehicleSpawner : MonoBehaviour
{
[SerializeField] GameObject vehicles;
[SerializeField] Vector3 offset;
[SerializeField] float offsetRange;
private void Start()
{
StartCoroutine(InsVehicle());
}
private Vector3 SetPosition()
{
offset = new Vector3(Random.Range(-offsetRange,offsetRange),offset.y,Random.Range(-offsetRange,offsetRange));
return MapManager.Instance.maps[4].transform.position + offset;
}
IEnumerator InsVehicle()
{
while(GameManager.Instance.State)
{
yield return new WaitForSeconds(5f);
Instantiate(vehicles, SetPosition() ,Quaternion.identity);
Debug.Log("생성");
}
}
}
코루틴 내에서는 5초마다 차량을 생성시킨다. 여기서 SetPosition함수를 사용하는데, 이 함수는 생성될 위치를 정해주는 함수로, offset에 랜덤으로 x좌표와 z좌표를 부여한 이후,
플레이어가 존재하는 maps[4]맵을 기준으로 생성시킨다.


(center의 position을 0,0,0으로 하고 부모 오브젝트에 넣어도 부모오브젝트의 중앙에 존재하지 않는다...)

차량이 캐릭터 위치에 따라 생성되는 것을 보았으니,
이번에는 차량의 위치를 구체적으로 정해줄 것이다.

[SerializeField] Transform[] carPosition;
VehicleSpawner에 생성될 좌표를 저장할 배열을 만들어 준다.

(하나하나씩 복붙해가면서 했는데 Transform을 사용하면 더 쉬웠을까..?)
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class VehicleSpawner : MonoBehaviour
{
[SerializeField] GameObject vehicles;
[SerializeField] Vector3 offset;
[SerializeField] float offsetRange;
[SerializeField] Vector3[] carPosition;
private int index;
private void Start()
{
StartCoroutine(InsVehicle());
}
private Vector3 SetPosition()
{
// 5 4
// 3 1
// 2 0
// 7 6
index = Random.Range(0, carPosition.Length);
return carPosition[index];
// offset = new Vector3(Random.Range(-offsetRange,offsetRange),offset.y,Random.Range(-offsetRange,offsetRange));
// Debug.Log(MapManager.Instance.maps[4].transform.position);
// Debug.Log(offset);
// return MapManager.Instance.maps[4].transform.position + offset;
}
private Quaternion SetRotation()
{
Quaternion direction = Quaternion.Euler(0, 0, 0);
switch(index / 2)
{
case 0: direction = Quaternion.Euler(0, -90, 0); break;
case 1: direction = Quaternion.Euler(0, 90, 0); break;
case 2: direction = Quaternion.Euler(0, 180, 0); break;
case 3: direction = Quaternion.Euler(0, 0, 0); break;
}
return direction;
}
IEnumerator InsVehicle()
{
while(GameManager.Instance.State)
{
yield return new WaitForSeconds(5f);
Instantiate(vehicles, SetPosition(), SetRotation());
//Instantiate(vehicles, SetPosition() ,Quaternion.identity);
Debug.Log("생성");
}
}
}
위에 index라는 변수를 선언해 주는데 이 변수는 내가 어떤 위치에 차량을 생성했는지를 보여준다.
(SetPosition함수 의 주석부분을 보면 알수있다.)
SetPosition에서 생성할 위치를 잡아주고, SetRotation에서는 위치에 따른 차량의 회전값을 정해주었다.

임시로 정해준 8개의 좌표가 부모 오브젝트없이 월드 포지션이기 때문에, maps[4]을 가져와도, 잘 호환되지 않는다. 그래서 임시로 이 좌표들을 맵의 자식 오브젝트로 넣어주고, 다시 좌표값을 받아올 것이다.

이러면 자신이 생성되야할 기준 맵이 바뀌어도 호환이 될 것이다.
private Vector3 SetPosition()
{
// 5 4
// 3 1
// 2 0
// 7 6
index = Random.Range(0, carPosition.Length);
return carPosition[index] + MapManager.Instance.maps[4].transform.position;
// offset = new Vector3(Random.Range(-offsetRange,offsetRange),offset.y,Random.Range(-offsetRange,offsetRange));
// Debug.Log(MapManager.Instance.maps[4].transform.position);
// Debug.Log(offset);
// return MapManager.Instance.maps[4].transform.position + offset;
}

(잘 안보이긴 하나 맵이 바뀌고나서 아예 다른곳에서 차량이 생성되었다.)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Vehicle : MonoBehaviour
{
private Rigidbody rigidBody;
private GameObject player;
[SerializeField] float speed;
private void Awake()
{
rigidBody = GetComponent<Rigidbody>();
player = GameObject.FindWithTag("Player");
}
private void FixedUpdate()
{
rigidBody.position += transform.forward * speed * Time.deltaTime;
}
}
이 스크립트는 자신을 앞으로 계속해서 움직이게 해주는 코드이다.
FixexUpdate에 Vector.forward를 사용하게 되면 월드 좌표를 기준으로 앞으로 가게되기에 로컬 좌표로 앞으로 이동하게 되는 transform.forward를 사용해 주었다.

이후 사용할 차량들에 필요한 컴포넌트를 추가해주었다.

여러가지의 차량들을 사용할 것이기 때문에, VehicleSpawner 스크립트도 수정해주었다.
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class VehicleSpawner : MonoBehaviour
{
[SerializeField] GameObject[] vehicles;
private GameObject SetObject()
{
return vehicles[Random.Range(0, vehicles.Length)];
}
IEnumerator InsVehicle()
{
while(GameManager.Instance.State)
{
yield return new WaitForSeconds(5f);
Instantiate(SetObject(), SetPosition(), SetRotation());
//Instantiate(vehicles, SetPosition() ,Quaternion.identity);
Debug.Log("생성");
}
}
}

이후 실행을 해보면 뭔가 이상한 점이 생기게 되는데,

이 이유는 맵을 만들 때, 차량들을 사용하였는데, 이때 사용한 차량 프리팹에도 움직일 차량에 넣은 컴포넌트들이 부착되어있기 때문에 움직인다.

하지만 이렇게만 하면, 맵에 있는 차량들이 움직이지만 않을뿐, 내장되어있는 스크립트가 아마 실행될 것이기때문에, 충돌과 같은 시스템을 위해서는 적합하지 않았다.


이후 생성된 프리팹들을 다시 Prefab폴더에 넣어준다.


이렇게 해주면 기존에 꾸미기 용도로 있던 차량들은 바뀌는게 없고,
움직일 차량들만 컴포넌트가 추가되고, 기능이 작동되는 것을 볼 수 있다.

때문에 기존에 차량들이 나왔던 8곳을 총 24곳으로 나누어 주었다.

때문에 이런식으로 배치하면 계속해서 차량이 여러군데에서 생성되는 것을 볼 수 있다.

[SerializeField] GameObject[] carPosition;
이유는 일일이 작성하는 것이 많아졌기에 귀찮기 때문이다. 이 코드로 GameObject를 가져오고 그 GameObject에 Position을 가져와 사용할 것이다.
private Vector3 SetPosition()
{
// 5 4
// 3 1
// 2 0
// 7 6
index = Random.Range(0, carPosition.Length);
return carPosition[index].transform.localPosition + MapManager.Instance.maps[4].transform.position;
// offset = new Vector3(Random.Range(-offsetRange,offsetRange),offset.y,Random.Range(-offsetRange,offsetRange));
// Debug.Log(MapManager.Instance.maps[4].transform.position);
// Debug.Log(offset);
// return MapManager.Instance.maps[4].transform.position + offset;
}
Vector3에서 GameObject가 되었기에 Position을 사용해야 하고, 게임오브젝트가 자식 오브젝트이기 때문에, transform.localPosition으로 작성해 주었다.
예를 들어 왼쪽을 바라봐야 하는 차량들 6개를 묶어 0~5배열 인덱스에 넣고 오른쪽을 바라봐야 하는 차량들 6개를 6~11배열 인덱스에 넣은 것 처럼 말이다.

private Quaternion SetRotation()
{
Quaternion direction = Quaternion.Euler(0, 0, 0);
switch(index / 6)
{
case 0: direction = Quaternion.Euler(0, -90, 0); break;
case 1: direction = Quaternion.Euler(0, 90, 0); break;
case 2: direction = Quaternion.Euler(0, 180, 0); break;
case 3: direction = Quaternion.Euler(0, 0, 0); break;
}
return direction;
}
IEnumerator InsVehicle()
{
while(GameManager.Instance.State)
{
yield return new WaitForSeconds(2f);
Instantiate(SetObject(), SetPosition(), SetRotation());
//Instantiate(vehicles, SetPosition() ,Quaternion.identity);
Debug.Log("생성");
}
}
생성 빈도를 대략적으로 맞춰주었다. (차량이 생성되는 빈도는 나중에 TimeManager에서 수정할 것이다.)


이후 Convex를 체크하여 충돌처리를 할수 있게 해준다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Vehicle : MonoBehaviour
{
private Rigidbody rigidBody;
private GameObject player;
[SerializeField] float speed;
private float destroyDistance = 300;
private void Awake()
{
rigidBody = GetComponent<Rigidbody>();
player = GameObject.FindWithTag("Player");
}
private void FixedUpdate()
{
rigidBody.position += transform.forward * speed * Time.deltaTime;
}
private void Update()
{
ScreenCheck();
}
void ScreenCheck()
{
if (Mathf.Abs(player.transform.position.x - transform.position.x) >= destroyDistance ||
Mathf.Abs(player.transform.position.z - transform.position.z) >= destroyDistance)
{
Destroy(gameObject);
}
}
}
ScreenCheck이라는 함수를 만들어서 Player를 기준으로 일정 이상 멀어지게 되면 차량을 삭제하는 기능을 추가해 주었다. 대략적으로 x 또는 z축으로 300이상 플레이어와 떨어지게 된다면 삭제되게끔 해주었다.

먼저 스크립트를 작성하기전에, 차량의 컴포넌트를 조금 수정해줄 것이다.

이 것을 방지하고자, 모든 차량의 Rigidbody컴포넌트에 y축 포지션 고정과, 질량을 수정해 주었다.

이제 직접 충돌 하였을때를 만들어 줄 것이다. 기본적으로 캐릭터의 Hp가 깎이면, 캐릭터를 밀쳐내는 스크립트를 만들어 줄 것이다.
private int contactDamage = -20;
private void OnCollisionEnter(Collision collision)
{
PlayerHealth playerHealth = collision.gameObject.GetComponent<PlayerHealth>();
if(playerHealth != null)
{
playerHealth.HealthUpdate(contactDamage);
}
}
먼저 OnCollisionEnter로 충돌하였을때를 검사하고, 캐릭터와 부딧혔을 때, contactDamage만큼 캐릭터의 Hp를 깎는 코드를 작성해 주었다.
private float contactPushPower = 100;
private void OnCollisionEnter(Collision collision)
{
PlayerHealth playerHealth = collision.gameObject.GetComponent<PlayerHealth>();
if(playerHealth != null)
{
playerHealth.HealthUpdate(contactDamage);
Vector3 contactNormal = collision.contacts[0].normal * contactPushPower;
collision.gameObject.GetComponent<Rigidbody>().AddForce(-contactNormal, ForceMode.Impulse);
}
}
밀려나는 크기는 100으로 잡아주었다.

여기서 살짝 수정할 점이 있다면, 정면으로 충돌할때, 충돌이 많아져 빠르게 hp가 깎인다는 점이다.
public bool isContactVehicle
{
get; private set;
} = false;`
public IEnumerator ContactVehicle()
{
isContactVehicle = true;
yield return new WaitForSeconds(1f);
isContactVehicle = false;
}
isContactVehicle이라는 bool형 변수를 선언해주고 이 변수가 true일때는 hp가 깎이지 않으며, false일 때만 hp가 깎이게 ContactVehicle 코루틴 함수를 작성해준다.
충돌을 하게 될 시, 코루틴 함수가 호출되고, 1초동안 hp가 깎이지 않게끔 함수를 만들어 준다.
private void OnCollisionEnter(Collision collision)
{
PlayerHealth playerHealth = collision.gameObject.GetComponent<PlayerHealth>();
if(playerHealth != null && !playerHealth.isContactVehicle)
{
playerHealth.HealthUpdate(contactDamage);
Vector3 contactNormal = collision.contacts[0].normal * contactPushPower;
collision.gameObject.GetComponent<Rigidbody>().AddForce(-contactNormal, ForceMode.Impulse);
StartCoroutine(playerHealth.ContactVehicle());
}
}
모든 충돌 시스템 이후에 ContactVehicle코루틴 함수를 호출시켜 바로 이후의 충돌을 막는다.
챠량이 캐릭터와 근접 하였을때와, 부딧혔을 때의 소리를 추가해 주도록 하겠다.
[SerializeField] AudioClip approachAudio;
[SerializeField] AudioClip contactAudio;
private float approachDistance = 30;
private bool isPlayapproachAudio = false;
각각, 캐릭터와 가까워졌을때 사운드, 부딧혔을 때 사운드,
가까워진 소리를 내는 거리, 가까워진 소리 1회 실행하게끔 하는 변수 이렇게 있다.
void ScreenCheck()
{
if (Mathf.Abs(player.transform.position.x - transform.position.x) <= approachDistance &&
Mathf.Abs(player.transform.position.z - transform.position.z) <= approachDistance)
{
if(!isPlayapproachAudio)
{
SoundManager.Instance.EffectPlay(approachAudio);
isPlayapproachAudio = true;
}
}
else if (Mathf.Abs(player.transform.position.x - transform.position.x) >= destroyDistance ||
Mathf.Abs(player.transform.position.z - transform.position.z) >= destroyDistance)
{
Destroy(gameObject);
}
}
위쪽에 if문을 생성시켜, 차량이 캐릭터와 가까워졌을때, 소리를 내는 코드를 작성해 준다.
여기서 중요한 점은 인접할 때, 소리를 내기때문에 OR연산자가 아닌 AND연산자를 사용해주었다.
private void OnCollisionEnter(Collision collision)
{
PlayerHealth playerHealth = collision.gameObject.GetComponent<PlayerHealth>();
if(playerHealth != null && !playerHealth.isContactVehicle)
{
playerHealth.HealthUpdate(contactDamage);
Vector3 contactNormal = collision.contacts[0].normal * contactPushPower;
collision.gameObject.GetComponent<Rigidbody>().AddForce(-contactNormal, ForceMode.Impulse);
SoundManager.Instance.EffectPlay(contactAudio);
StartCoroutine(playerHealth.ContactVehicle());
}
}
충돌되었을때 (무적상태X), 충돌하는 소리를 낼 것이다.




가끔가다가 차량들이 노선을 이탈하는 일이 생기는데 이 경우 차량들끼리의 충돌을 했기 때문이다.
이를 고치기 위해서 간단하게 차량끼리의 충돌을 제거해 줄 것이다.


Vehicle레이어와 Vehicle 레이어간의 충돌을 체크해제 해준다.

MissingReferenceException: The variable maps of MapManager doesn't exist anymore.

return carPosition[index].transform.localPosition + MapManager.Instance.maps[4].transform.position;
애초에 MapManager라는 클래스는 GameScene에서만 존재해야 했기에 잘 수정한 것 같다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Map : MonoBehaviour
{
public int number;
MapManager mapManager;
private void Awake()
{
mapManager = FindObjectOfType<MapManager>();
}
private void OnTriggerEnter(Collider other)
{
if(other.gameObject.CompareTag("Player"))
{
mapManager.MapUpdate(number);
}
}
}
[SerializeField] GameObject[] carPosition;
private void Awake()
{
mapManager = FindObjectOfType<MapManager>();
}
private Vector3 SetPosition()
{
index = Random.Range(0, carPosition.Length);
return carPosition[index].transform.localPosition + mapManager.maps[4].transform.position;
}
아이템의 효과의 경우 총 3가지로 생각해 봤는데, 이동속도가 빨라지는 아이템, 캐릭터의 크기가 커지는 아이템, 음식과 차량의 움직임을 정지시키는 아이템 이렇게 총 세가지로 할 것이다.

상자같은 표면을 위해 Material의 Albedo, Metallic, Smoothness를 설정해 주었다.

이후 상자의 아랫면을 제외한 곳에 이미지를 생성시킨다.

사용할 이미지들은 Sprites폴더를 만들어 이곳에 넣어준 이후 Texture타입을 Sprite로 바꿔준 이후 사용한다.

먼저 작성해 줄 것은 아이템을 생성시키는 ItemSpawner 라는 스크립트를 만들 것이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSpawner : MonoBehaviour
{
[SerializeField] GameObject[] itemBoxes;
private void Start()
{
StartCoroutine(InsItemBox());
}
private GameObject SetObject()
{
return itemBoxes[Random.Range(0, itemBoxes.Length)];
}
void SetPosition()
{
}
IEnumerator InsItemBox()
{
while(GameManager.Instance.State)
{
// Instantiate(SetObject,);
yield return new WaitForSeconds(15f);
}
}
}
아이템의 경우 도로에만 떨어지도록 설계를 해야 하기 때문에,
떨어질 위치에 대한 범위를 지정해줄 필요가 있다.

챠량의 경우 포지션을 여러개로 정해주고 정해진 곳 에서 나오게 했지만, 아이템의 경우 세로 3줄 가로 3줄해서 총 6개의 도로의 x의 최대,최소값, z의 최대 최소값을 구하고 사용할 것이다.
물론, 차량생성처럼 현재 중앙맵을 사용하여 작성할 것이다.



이어서 방금 생성한 아이템 박스 오브젝트를 복사하여 6개의 도로의 범위를 구해준다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSpawner : MonoBehaviour
{
[SerializeField] GameObject[] itemBoxes;
[SerializeField] GameObject[] itemPosition;
private void Start()
{
StartCoroutine(InsItemBox());
}
private GameObject SetObject()
{
return itemBoxes[Random.Range(0, itemBoxes.Length)];
}
Vector3 SetPosition()
{
int roadNumber = Random.Range(0, itemPosition.Length / 2);
Vector3 first = itemPosition[roadNumber * 2].transform.position;
Vector3 second = itemPosition[roadNumber * 2 + 1].transform.position;
float minX = Mathf.Min(first.x, second.x);
float maxX = Mathf.Max(first.x, second.x);
float minZ = Mathf.Min(first.z, second.z);
float maxZ = Mathf.Max(first.z, second.z);
return new Vector3(Random.Range(minX, maxX), first.y, Random.Range(minZ, maxZ)); ;
}
IEnumerator InsItemBox()
{
while(GameManager.Instance.State)
{
Instantiate(SetObject(),SetPosition(),Quaternion.identity);
yield return new WaitForSeconds(3f);
}
}
}
먼저 itemPosition으로 아이템의 범위를 받아준다. 총 12개의 오브젝트를 넣게 된다.
이후에 SetPosition을 살펴보면 먼저 roadNumber로 도로의 번호를 뽑게된다. 도로마다 2개의 상자로 범위가 이루어져 있어, 나누기 2로 6개의 도로만 뽑을 수 있도록 한다.
minX, maxX, minZ, maxZ를 통해서 도로에 위치한 각각의 최솟값, 최댓값을 구해낸 이후,
최솟값과 최댓값 사이의 랜덤한 위치로 아이템 박스를 생성시킨다.



이제 맵의 변화에 맞게끔 생성되게 해줄 것이다. VehicleSpawner처럼 작성해주면 된다.
MapManager mapManager;
private void Awake()
{
mapManager = FindObjectOfType<MapManager>();
}
Vector3 SetPosition()
{
int roadNumber = Random.Range(0, itemPosition.Length / 2);
Vector3 first = itemPosition[roadNumber * 2].transform.localPosition;
Vector3 second = itemPosition[roadNumber * 2 + 1].transform.localPosition;
float minX = Mathf.Min(first.x, second.x);
float maxX = Mathf.Max(first.x, second.x);
float minZ = Mathf.Min(first.z, second.z);
float maxZ = Mathf.Max(first.z, second.z);
Vector3 boxPosition = new Vector3(Random.Range(minX, maxX), first.y, Random.Range(minZ, maxZ));
return boxPosition + mapManager.maps[4].transform.position;
}
먼저 MapManager를 사용할 것이기때문에 선언과 할당을 해주고,
이 이후에 유니티에서 아이템생성 위치를 Maps[4] 오브젝트에 넣어 줄 것이기 때문이다.
이후에 boxPosition이라는 코드에 랜덤한 값을 할당하고,
값을 리턴 할때에는 중아에 있는 맵의 좌표를 더해서 반환한다.

(차량의 스폰 위치를 CarPosition으로 묶고, 아이템 스폰 위치도 동일하게 정리하였다.)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSpawner : MonoBehaviour
{
[SerializeField] GameObject[] itemBoxes;
[SerializeField] GameObject[] itemPosition;
MapManager mapManager;
private void Awake()
{
mapManager = FindObjectOfType<MapManager>();
}
private void Start()
{
StartCoroutine(InsItemBox());
}
private GameObject SetObject()
{
return itemBoxes[Random.Range(0, itemBoxes.Length)];
}
Vector3 SetPosition()
{
int roadNumber = Random.Range(0, itemPosition.Length / 2);
Vector3 first = itemPosition[roadNumber * 2].transform.localPosition;
Vector3 second = itemPosition[roadNumber * 2 + 1].transform.localPosition;
float minX = Mathf.Min(first.x, second.x);
float maxX = Mathf.Max(first.x, second.x);
float minZ = Mathf.Min(first.z, second.z);
float maxZ = Mathf.Max(first.z, second.z);
Vector3 boxPosition = new Vector3(Random.Range(minX, maxX), first.y, Random.Range(minZ, maxZ));
return boxPosition + mapManager.maps[4].transform.position;
}
IEnumerator InsItemBox()
{
while(GameManager.Instance.State)
{
Instantiate(SetObject(),SetPosition(),Quaternion.identity);
yield return new WaitForSeconds(5f);
}
}
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class Item : MonoBehaviour
{
protected float effectTime;
[SerializeField] float fallSpeed;
protected abstract void Use();
protected void Update()
{
FallDown();
}
protected void FallDown()
{
transform.Translate(Vector3.down * fallSpeed * Time.deltaTime);
}
}
먼저 아이템들의 효과시간, 떨어지는 속도등을 변수로 가진다.
FallDown이라는 함수로 상속받는 객체들은 모두 계속해서 떨어지게끔 만들어 주었다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GiantItem : Item
{
protected override void Use()
{
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpeedItem : Item
{
protected override void Use()
{
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TimeItem : Item
{
protected override void Use()
{
}
}

[SerializeField] float itemPosY;
Vector3 SetPosition()
{
int roadNumber = Random.Range(0, itemPosition.Length / 2);
Vector3 first = itemPosition[roadNumber * 2].transform.localPosition;
Vector3 second = itemPosition[roadNumber * 2 + 1].transform.localPosition;
float minX = Mathf.Min(first.x, second.x);
float maxX = Mathf.Max(first.x, second.x);
float minZ = Mathf.Min(first.z, second.z);
float maxZ = Mathf.Max(first.z, second.z);
Vector3 boxPosition = new Vector3(Random.Range(minX, maxX), itemPosY, Random.Range(minZ, maxZ));
return boxPosition + mapManager.maps[4].transform.position;
}
이후에 아이템이 생성될 위치를 가지는 변수를 잡아주고 위치를 생성시킬때, 이 높이로 생성시킨다.


다만 아이템이 바닥을 뚫게 되는데
이 경우 아이템 프리팹에 Rigidbody를 넣고 코드를 수정하여 못 뚫게 해줄 것이다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class Item : MonoBehaviour
{
Rigidbody rigidBody;
protected float effectTime;
[SerializeField] float fallSpeed;
protected abstract void Use();
protected void Awake()
{
rigidBody = GetComponent<Rigidbody>();
}
protected void FixedUpdate()
{
FallDown();
}
protected void FallDown()
{
rigidBody.MovePosition(rigidBody.position + Vector3.down * fallSpeed * Time.deltaTime);
}
}
먼저 Rigidbody를 사용하는 물체인 만큼 Rigidbody선언과 할당을 해주고,
Update가 아닌 FixedUpdate에서 물리 연산을 해준다.
transform.Translate가 아닌 MovePosition으로 콜라이더끼리의 접촉에서 유리하도록 바꿔준다.

(만약 FixedUpdate가 아닌 Update에서 했다면 덜덜덜 떨리는 모습을 보인다.)
먼저 가장쉬운 SpeedItem을 만들 것이다 (SpeedItem -> GiantItem -> TimeItem순)
SpeedItem를 작성하기 전에 부모클래스를 수정할 것이 있는데,
protected virtual void Awake()
{
rigidBody = GetComponent<Rigidbody>();
}
Awake 함수에 virtual 키워드를 붙혀주었다.
Awake의 같은 생명주기 함수들은 많이 사용하기 때문에 virtual로 선언하고 자식 클래스에서 base.~~~() 이후 재정의를 통해 확장하는 식으로 사용할 것이기 때문에 이렇게 수정해주었다.
public float speed;
그리고 여기서 speed 변수의 접근 지정자를 [SerializeField]에서 public으로 바꿔 접근 할수 있도록 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpeedItem : Item
{
PlayerMove playerMove;
protected override void Awake()
{
base.Awake();
playerMove = FindObjectOfType<PlayerMove>();
effectTime = 5;
}
protected override void Use()
{
StartCoroutine(SpeedUp());
}
IEnumerator SpeedUp()
{
playerMove.speed += 20;
yield return new WaitForSeconds(effectTime);
playerMove.speed -= 20;
}
}
PlayerMove에 있는 speed 변수를 사용할 것이기 때문에 PlayerMove변수를 선언및 할당해준다.
effectTime은 임의적으로 5초로 잡아주었다.
캐릭터가 아이템을 먹으면 SpeedUp 코루틴 함수가 실행되고 5초간 이동속도가 20빨리지게 되어있다.
PlayerMove 스크립트에서 speed가 아닌 TimeManager의 speed를 사용한다는 점이였다.
그래서 TimeManager와 PlayerMove 스크립트에서의 speed 구조를 수정할 것이다.
void Move()
{
rigidBody.MovePosition(rigidBody.position + playerInput.Move * speed * Time.deltaTime);
}
먼저 TimeManager의 playerSpeed 대신 자신의 변수인 speed를 사용하게끔 수정해준다.
TimeManager에서 PlayerMove의 속도값을 정해주는게 아니라, PlayerMove에서 기본 속도 값이 있고, TimeManager에서는 그 값에 추가 이속을 더해주는 식으로 작성 해줄 것이다.
private PlayerMove playerMove;
private void Awake()
{
instance = this;
playerMove = FindObjectOfType<PlayerMove>();
}
public float PlayerSpeed
{
get { return playerSpeed; }
set
{
playerSpeed = value;
if(playerSpeed >= 15)
{
playerSpeed = 15;
}
playerMove.speed += playerSpeed - playerMove.speed;
}
}
먼저 playerMove의 speed값을 사용하기 위해 선언 및 할당을 해준다.
이후 PlayerSpeed 프로퍼티에 코드 한줄을 추가해 주는데, PlayerSpeed의 값이 변고, 한계값을 적용한 이후에 PlayerMove의 speed 값에 현재 이속 - 기본 이속 을 통하여, 추가 된 값만 보낸다.
이렇게 작성하게되면, 레벨디자인의 변화가 존재하지 않고, 두 speed값을 분리시킬 수 있다.
OnCollisionEnter를 Item 부모클래스에 적어줄 계획이다.
private void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.CompareTag("Player"))
{
Use();
Debug.Log("실행");
}
}
Item 스크립트에 OnCollisionEnter 함수를 추가해준다. virtual을 사용하지 않은 이유는 모든 Item은 부모클래스의 OnCollisionEnter의 로직과 동일하게 행동할 것이기 때문에 굳이 재정의를 하진 않았다.
만약 Player 태그를 가진 오브젝트와 부딧친다면 추상함수 Use를 호출해 아이템 사용을 실행한다.

자이언트 아이템의 경우 캐릭터의 크기가 커지고, 플레이어의 화면이 축소된다.
이로써 큰 음식 획득 범위로 많은 음식을 먹을 수 있고, 더 많은 시야 정보를 제공받는다.
타임 아이템의 능력은 음식(아이템)과 차량, 체력을 잠시동안 정지시킨다.
이로써 스코어만 계속 올라가는 상황과, 체력이 없을 때 일시적으로 구사일생이 될수 있는 아이템이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GiantItem : Item
{
private CameraMove cameraMove;
private GameObject player;
protected override void Awake()
{
base.Awake();
cameraMove = FindObjectOfType<CameraMove>();
player = GameObject.FindWithTag("Player");
effectTime = 7;
}
protected override void Use()
{
StartCoroutine(SizeUp());
}
IEnumerator SizeUp()
{
float size = Random.Range(1f, 10f);
Debug.Log(size);
Vector3 sizeVec = new Vector3(size, size, size);
while(player.transform.localScale.x + 0.001f <= sizeVec.x)
{
player.transform.localScale = Vector3.Lerp(player.transform.localScale, sizeVec, 0.2f);
yield return null;
}
yield return new WaitForSeconds(effectTime);
sizeVec = Vector3.one;
while(player.transform.localScale.x - 0.001f >= 1)
{
player.transform.localScale = Vector3.Lerp(player.transform.localScale, sizeVec, 0.2f);
yield return null;
}
}
}
먼저 몸이 커지기 때문에 캐릭터를 저장할 player와 카메라의 위치를 잡아줘야 하기에 CameraMove를 선언 및 할당 해주었다.
effectTime 같은 경우 일단은 7초로 잡아주었다.
자이언트 상자와 접촉하면 SizeUP이라는 코루틴 함수를 실행하는데,
먼저 1~10배중 랜덤한 값으로 몸의 크기가 변한다. 이후 지속시간이 지나면 다시 1로 작아지는 코드이다.
자연스럽게 몸이 변화하기 위해 Lerp를 사용해주었다.
아직 카메라를 구현하지 않은 상태이다.
여기서 살짝 아쉬웠던 점은 player의 경우 OnCollision으로 접촉했을 때만 오브젝트를 가져오면 될 거 같은데 OnCollision이라는 함수는 virtual이나 abstract이 안되고, 자식 오브젝트에서 또 작성하게 된다면, 부모 클래스의 함수가 무시 되기 때문에 그나마 내가 생각한 방법으론 생성되자마자 player찾기로 할당해주었다..

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraMove : MonoBehaviour
{
[SerializeField] GameObject target;
public Vector3 positionOffset;
private void LateUpdate()
{
transform.position = target.transform.position + positionOffset;
}
}
positionOffest의 값에 변화를 주어 플레이어를 쳐다보도록 하기 위해 접근 지정자를 public으로 수정한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GiantItem : Item
{
private CameraMove cameraMove;
private GameObject player;
Vector3 oneCameraTransform;
private float size;
protected override void Awake()
{
base.Awake();
cameraMove = FindObjectOfType<CameraMove>();
player = GameObject.FindWithTag("Player");
effectTime = 7;
oneCameraTransform = cameraMove.positionOffset;
}
protected override void Use()
{
size = Random.Range(1f, 10f);
StartCoroutine(SizeUp());
StartCoroutine(CameraBack());
}
IEnumerator SizeUp()
{
Vector3 sizeVec = new Vector3(size, size, size);
while (player.transform.localScale.x + 0.001f <= sizeVec.x)
{
player.transform.localScale = Vector3.Lerp(player.transform.localScale, sizeVec, 0.2f);
yield return null;
}
yield return new WaitForSeconds(effectTime);
Debug.Log("크기 끝");
sizeVec = Vector3.one;
while(player.transform.localScale.x - 0.001f >= 1)
{
player.transform.localScale = Vector3.Lerp(player.transform.localScale, sizeVec, 0.2f);
yield return null;
}
}
IEnumerator CameraBack()
{
Vector3 endCameraTransform = oneCameraTransform + new Vector3(0, (size + size * 0.5f), -size);
while (cameraMove.positionOffset.y + 0.001f <=endCameraTransform.y)
{
cameraMove.positionOffset = Vector3.Lerp(cameraMove.positionOffset, endCameraTransform, 0.2f);
yield return null;
}
yield return new WaitForSeconds(effectTime);
while(cameraMove.positionOffset.y - 0.001f > oneCameraTransform.y)
{
cameraMove.positionOffset = Vector3.Lerp(cameraMove.positionOffset, oneCameraTransform, 0.2f);
yield return null;
}
cameraMove.positionOffset = oneCameraTransform;
}
}
위에서 부터 살펴보자면 oneCameraTransform 같은 경우 원래의 카메라 오프셋 (캐릭터의 크기가 1일때)를 저장해 주는 변수이고
size같은 경우, 기존엔 SizeUp 코루틴 함수에서 랜덤 값으로 정해줬는데, 두 개의 함수에서 사용하기 때문에 전역변수로 정해주었다.
Awake안에서는 oneCameraTransform에 원래의 카메라 오프셋을 저장해준다.
Use함수의 경우 size의 값을 랜덤으로 할당해 주고, CameraBack이라는 코루틴함수를 뒤에 호출시켜준다.
SizeUp함수의 경우 size의 값을 할당해주는 코드가 없어졌다.
중요한 CameraBack 코루틴 함수의 경우 먼저 캐릭터의 사이즈가 바뀌고 도착할 위치를 endCameraTransform 으로 정해주었다. 처음 카메라의 위치에서 인위적으로 size값에 비례하여 위치가 크기가 변하는 Vector3를 더해주었다. (size값이 클수록 높이가 올라가고, 캐릭터에서 멀어진다.)
이후 Vector3.Lerp를 이용하여 자연스럽게 카메라의 위치가 변경되고, 지속시간을 기다렸다가, 다시 자연스럽게 카메라의 위치가 원래 있던 오프셋으로 이동하는 것을 볼수 있다.
혹시모를 버그를 위해 마지막에 직접적으로 카메라 오프셋의 좌표를 지정해 주었다.

세부적인 사항같은 경우 캐릭터가 커지는 코루틴과 카메라가 움직이는 코루틴간의 지속시간이 끝나는 시간이 동일 하지않을 때도 있다. (각각 처음 while문을 통과하는 시간이 다르다.)
가장 만들기 힘들어서 마지막에 둔 만큼 참조해야할 것이 매우 많다.
TimeItem의 경우 떨어지는 음식,아이템, 차량의 움직임, 체력감소가 일정 시간동안 정지하게된다.
이 아이템을 먹었을때, 일시적으로 스코어만 올라가고, 땅과 가까운 음식을 먹어 체력을 회복할수 있는 매리트가 존재한다.
먼저 음식을 만들어 볼 것인데, Food의 떨어지는 속도를 0으로 만들어 줘야 하는데, food의 경우 TimeManager의 속도를 사용하기 때문에, 이를 수정 해줄 것이다.
public float fallSpeed; // 떨어지는 속도
private void Start()
{
fallSpeed += TimeManager.Instance.FoodFallSpeed - fallSpeed;
}
private void Update()
{
transform.position += Vector3.down * fallSpeed * Time.deltaTime;
}
처음에 fallSpeed의 변수를 다른 곳에서 할당할 수 있도록 접근지정자를 public으로 바꿔준다.
Start에서 fallSpeed를 재정의 해준다. 기존 속도에 TimeManager의 속도 - 현재 속도를 해줌으로써,
TimeManager에서 계속 증가하는 FoodFallSpeed의 기존 값(5) 말고 증가하는 값(기존값 * 1.075)만 추가해주며, TimeManager에서 수정하지 않은 이유는 Food라는 객체는 여러개이기 때문이다.
이후에 Update에서 fallSpeed를 사용하여 떨어지는 속도를 정의해준다.
public bool isCreateTime = true;
public IEnumerator CreateFood()
{
while(GameManager.Instance.State && isCreateTime)
{
InsFoodPosition = new Vector3(Random.Range(-foodX, foodX), foodY, Random.Range(-foodZ, foodZ)) + target.transform.position;
Instantiate(foods[Random.Range(0, foods.Length)], InsFoodPosition, Quaternion.identity);
yield return new WaitForSeconds(TimeManager.Instance.FoodFallTime);
}
}
이 경우 bool 변수를 이용하여 만들어 주었다. isCreateTime이 true일 때만 생성키기는 코드를 음식을 생성시키는 코루틴 함수의 while문에 추가하여 생성의 유무를 쉽게 접근할 수 있도록 하였다.
또한 CreateFood의 접근 지정자를 public으로 바꿔주었는데, isCreateTime이 false가 되어 코루틴 함수가 중단되면 다시 생성시킬 수 없는데 TimeItem에서 다시 이 코루틴 함수를 실행시켜줄 것이기 때문이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
public class TimeItem : Item
{
private FoodSpawner foodSpawner;
private List<Food> foods = new List<Food>();
private List<float> foodsFallSpeed = new List<float>();
protected override void Awake()
{
base.Awake();
foodSpawner = FindObjectOfType<FoodSpawner>();
effectTime = 7;
}
protected override void Use()
{
StartCoroutine(TimeStop());
}
IEnumerator TimeStop()
{
FoodStop();
yield return new WaitForSeconds(effectTime);
FoodStart();
}
void FoodStop()
{
foods = new List<Food>(FindObjectsOfType<Food>());
for(int i = 0;i < foods.Count; i++)
{
foodsFallSpeed.Add(foods[i].fallSpeed);
foods[i].GetComponent<Food>().fallSpeed = 0;
}
foodSpawner.isCreateTime = false;
}
void FoodStart()
{
for(int i = 0;i < foods.Count; i++)
{
if (foods[i] != null)
{
foods[i].GetComponent<Food>().fallSpeed = foodsFallSpeed[i];
}
}
foodSpawner.isCreateTime = true;
StartCoroutine(foodSpawner.CreateFood());
}
}
위에서 부터 설명하자면,
List인 foods의 경우 현재 있는 음식들을 담아둘 list이며, foodsFallSpeed의 경우 그 음식들의 속도를 저장해주는 list이다.
Awake에서 foodSpawner를 할당하고 effectTime을 7로 설정해준다.
Use함수가 호출되면 TimeStop 코루틴함수를 호출하는데 TimeStop의 경우 FoodStop으로 음식을 이동을 정지시키는 함수 이후 지속시간 이후에 FoodStart라는 다시 음식을 이동시키는 함수를 호출한다.
FoodStop 함수의 경우 먼저 foods로 현재 있는 Food 컴포넌트를 넣어준 다음 for문을 통해 foodFallSpeed에 현재 떨어지는 속도를 넣어준다음 속도를 0으로 바꿔준다.
이후 foodSpawner의 isCreateTime을 false로 로 바꿔준다. 이러면 음식을 생성을 하지 못하게된다.
FoodStart 함수의 경우 for문을 이용하여 foods에 있는 음식들의 속도를 foodsFallSpeed를 사용하여 원상복수 시키는데, 이때 if문을 통해, 음식이 존재하는지를 판단해 준다. 이 이유는 음식이 정지했을 때, 플레이어가 음식을 먹어버린다면 참조할 수 없기 때문에, 이미 먹을 음식은 패스해준다고 볼 수 있다.
이후 다시 isCreateTime을 true로 바꿔 음식 생성 조건을 활성화 시키고, 중단되었던 CreateFood 코루틴 함수를 실행 시켜주며, 다시 음식을 생성시키게끔 해준다.

기본적으로 Food와 로직 방식이 비슷하기 때문에, 비교적 만들어 주기 간단하다.
Food를 만들었던 코드와 유사하기에 설명은 하지 않겠다.
public bool isCreateTime = true;
public IEnumerator InsItemBox()
{
while(GameManager.Instance.State && isCreateTime)
{
Instantiate(SetObject(),SetPosition(),Quaternion.identity);
yield return new WaitForSeconds(1f);
}
}
public float fallSpeed;
private ItemSpawner itemSpawner;
private List<Item> items = new List<Item>();
private List<float> itemsFallSpeed = new List<float>();
protected override void Awake()
{
base.Awake();
foodSpawner = FindObjectOfType<FoodSpawner>();
itemSpawner = FindObjectOfType<ItemSpawner>();
effectTime = 7;
}
IEnumerator TimeStop()
{
FoodStop();
ItemStop();
yield return new WaitForSeconds(effectTime);
FoodStart();
ItemStart();
}
void ItemStop()
{
items = new List<Item>(FindObjectsOfType<Item>());
for(int i = 0;i < items.Count;i++)
{
itemsFallSpeed.Add(items[i].fallSpeed);
items[i].fallSpeed = 0;
}
itemSpawner.isCreateTime = false;
}
void ItemStart()
{
for(int i = 0;i < items.Count; i++)
{
if (items[i] != null)
{
items[i].fallSpeed = itemsFallSpeed[i];
}
}
itemSpawner.isCreateTime = true;
StartCoroutine(itemSpawner.InsItemBox());
}
차량같은 경우에도 아이템을 먹으면 생성금지, 움직임 금지를 하면 되기에 앞선 코드들과 비슷하다.
역시나 코드의 설명은 하지 않겠다.
public float speed;
public IEnumerator InsVehicle()
{
while(GameManager.Instance.State && isCreateTime)
{
yield return new WaitForSeconds(2f);
Instantiate(SetObject(), SetPosition(), SetRotation());
//Instantiate(vehicles, SetPosition() ,Quaternion.identity);
// Debug.Log("생성");
}
}
private VehicleSpawner vehicleSpawner;
private List<Vehicle> vehicles = new List<Vehicle>();
private List<float> vehiclesSpeed = new List<float>();
protected override void Awake()
{
base.Awake();
foodSpawner = FindObjectOfType<FoodSpawner>();
itemSpawner = FindObjectOfType<ItemSpawner>();
vehicleSpawner = FindObjectOfType<VehicleSpawner>();
effectTime = 7;
}
void VehicleStop()
{
vehicles = new List<Vehicle>(FindObjectsOfType<Vehicle>());
for(int i = 0;i < vehicles.Count; i++)
{
vehiclesSpeed.Add(vehicles[i].speed);
vehicles[i].speed = 0;
}
vehicleSpawner.isCreateTime = false;
}
void VehicleStart()
{
for(int i = 0;i < vehicles.Count; i++)
{
if (vehicles[i] != null)
{
vehicles[i].speed = vehiclesSpeed[i];
}
}
vehicleSpawner.isCreateTime = true;
StartCoroutine(vehicleSpawner.InsVehicle());
}
PlayerHealth 스크립트는 씬에 하나만 존재하기 때문에 더욱 간단하다.
public bool isDecreaseTime = true;
public IEnumerator DecreaseHealth()
{
while (GameManager.Instance.State && isDecreaseTime)
{
yield return new WaitForSeconds(TimeManager.Instance.DecreaseHpTime);
HealthUpdate(-decreaseHealthValue);
}
}
(public으로 된 변수의 경우 초기값이 적절하지 않을수도 있으니, Inspector에서 한번 확인하자...!)
private PlayerHealth playerHealth;
protected override void Awake()
{
base.Awake();
foodSpawner = FindObjectOfType<FoodSpawner>();
itemSpawner = FindObjectOfType<ItemSpawner>();
vehicleSpawner = FindObjectOfType<VehicleSpawner>();
playerHealth = FindObjectOfType<PlayerHealth>();
effectTime = 7;
}
IEnumerator TimeStop()
{
FoodStop();
ItemStop();
VehicleStop();
HealthStop();
yield return new WaitForSeconds(effectTime);
FoodStart();
ItemStart();
VehicleStart();
HealthStart();
}
void HealthStop()
{
playerHealth.isDecreaseTime = false;
}
void HealthStart()
{
playerHealth.isDecreaseTime = true;
StartCoroutine(playerHealth.DecreaseHealth());
}
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UIElements;
public class TimeItem : Item
{
private FoodSpawner foodSpawner;
private List<Food> foods = new List<Food>();
private List<float> foodsFallSpeed = new List<float>();
private ItemSpawner itemSpawner;
private List<Item> items = new List<Item>();
private List<float> itemsFallSpeed = new List<float>();
private VehicleSpawner vehicleSpawner;
private List<Vehicle> vehicles = new List<Vehicle>();
private List<float> vehiclesSpeed = new List<float>();
private PlayerHealth playerHealth;
protected override void Awake()
{
base.Awake();
foodSpawner = FindObjectOfType<FoodSpawner>();
itemSpawner = FindObjectOfType<ItemSpawner>();
vehicleSpawner = FindObjectOfType<VehicleSpawner>();
playerHealth = FindObjectOfType<PlayerHealth>();
effectTime = 7;
}
protected override void Use()
{
StartCoroutine(TimeStop());
}
IEnumerator TimeStop()
{
FoodStop();
ItemStop();
VehicleStop();
HealthStop();
yield return new WaitForSeconds(effectTime);
FoodStart();
ItemStart();
VehicleStart();
HealthStart();
}
void FoodStop()
{
foods = new List<Food>(FindObjectsOfType<Food>());
for(int i = 0;i < foods.Count; i++)
{
foodsFallSpeed.Add(foods[i].fallSpeed);
foods[i].GetComponent<Food>().fallSpeed = 0;
}
foodSpawner.isCreateTime = false;
}
void FoodStart()
{
for(int i = 0;i < foods.Count; i++)
{
if (foods[i] != null)
{
foods[i].GetComponent<Food>().fallSpeed = foodsFallSpeed[i];
}
}
foodSpawner.isCreateTime = true;
StartCoroutine(foodSpawner.CreateFood());
}
void ItemStop()
{
items = new List<Item>(FindObjectsOfType<Item>());
for(int i = 0;i < items.Count;i++)
{
itemsFallSpeed.Add(items[i].fallSpeed);
items[i].fallSpeed = 0;
}
itemSpawner.isCreateTime = false;
}
void ItemStart()
{
for(int i = 0;i < items.Count; i++)
{
if (items[i] != null)
{
items[i].fallSpeed = itemsFallSpeed[i];
}
}
itemSpawner.isCreateTime = true;
StartCoroutine(itemSpawner.InsItemBox());
}
void VehicleStop()
{
vehicles = new List<Vehicle>(FindObjectsOfType<Vehicle>());
for(int i = 0;i < vehicles.Count; i++)
{
vehiclesSpeed.Add(vehicles[i].speed);
vehicles[i].speed = 0;
}
vehicleSpawner.isCreateTime = false;
}
void VehicleStart()
{
for(int i = 0;i < vehicles.Count; i++)
{
if (vehicles[i] != null)
{
vehicles[i].speed = vehiclesSpeed[i];
}
}
vehicleSpawner.isCreateTime = true;
StartCoroutine(vehicleSpawner.InsVehicle());
}
void HealthStop()
{
playerHealth.isDecreaseTime = false;
}
void HealthStart()
{
playerHealth.isDecreaseTime = true;
StartCoroutine(playerHealth.DecreaseHealth());
}
}

private bool isContact = false;
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Player"))
{
Use();
ItemActive();
isContact = true;
Debug.Log("실행");
}
else if (collision.gameObject.CompareTag("Ground"))
{
StartCoroutine(ItemDestroy(5f));
}
}
void ItemActive()
{
gameObject.GetComponent<MeshRenderer>().enabled = false;
gameObject.GetComponent<Collider>().enabled = false;
transform.GetChild(0).gameObject.SetActive(false);
}
protected IEnumerator ItemDestroy(float time)
{
yield return new WaitForSeconds(time);
if(!isContact)
{
Destroy(gameObject);
}
}
먼저 isContact라는 변수를 선언해주는데, 이 변수의 경우 이 아이템이 캐릭터와 부딧혔는지 판단한다.
플레이어와 닿았을 때와 바닥에 닿았을 때 다른 실행을 하는데,
플레이어와 닿았을 때, 먼저 isContact를 true로 바꿔주고, ItemActive 함수를 호출하여, 아이템의 비주얼과, 콜라이더, 자식오브젝트를 비활성화 시켜 삭제가 된 것처럼 해준다.
이 이유는 아이템을 바로 삭제시키면 아이템의 코드가 실행되지 않기 때문이다.
바닥에 닿았을 때는 ItemDestroy 코루틴 함수를 통해, 5초가 지날때 까지 캐릭터와 부딧히지 않았다면 아이템을 삭제시킨다.
플레이어와 닿았을 때는 자식 클래스의 실행이 모두 끝났을 때, ItemDestroy를 호출시킨다.
그러므로 자식 클래스도 수정해 준다.
IEnumerator TimeStop()
{
FoodStop();
ItemStop();
VehicleStop();
HealthStop();
yield return new WaitForSeconds(effectTime);
FoodStart();
ItemStart();
VehicleStart();
HealthStart();
StartCoroutine(ItemDestroy(0));
}
지속시간이 지나 모든 실행이 끝나고서야 아이템을 즉시 삭제시킨다.
IEnumerator SpeedUp()
{
playerMove.speed += 20;
yield return new WaitForSeconds(effectTime);
playerMove.speed -= 20;
StartCoroutine(ItemDestroy(0));
}
private bool isSizeUp = false;
private bool isCameraBack = false;
private void Update()
{
if(isSizeUp && isCameraBack)
{
StartCoroutine(ItemDestroy(0));
}
}
IEnumerator SizeUp()
{
Vector3 sizeVec = new Vector3(size, size, size);
while (player.transform.localScale.x + 0.001f <= sizeVec.x)
{
player.transform.localScale = Vector3.Lerp(player.transform.localScale, sizeVec, 0.2f);
yield return null;
}
yield return new WaitForSeconds(effectTime);
Debug.Log("크기 끝");
sizeVec = Vector3.one;
while(player.transform.localScale.x - 0.001f >= 1)
{
player.transform.localScale = Vector3.Lerp(player.transform.localScale, sizeVec, 0.2f);
yield return null;
}
isSizeUp = true;
}
IEnumerator CameraBack()
{
Vector3 endCameraTransform = oneCameraTransform + new Vector3(0, (size + size * 0.5f), -size);
while (cameraMove.positionOffset.y + 0.001f <=endCameraTransform.y)
{
cameraMove.positionOffset = Vector3.Lerp(cameraMove.positionOffset, endCameraTransform, 0.2f);
yield return null;
}
yield return new WaitForSeconds(effectTime);
while(cameraMove.positionOffset.y - 0.001f > oneCameraTransform.y)
{
cameraMove.positionOffset = Vector3.Lerp(cameraMove.positionOffset, oneCameraTransform, 0.2f);
yield return null;
}
cameraMove.positionOffset = oneCameraTransform;
isCameraBack = true;
}
GiantItem의 경우 하나의 코루틴으로 지속시간을 제어하지 않기 때문에 bool변수 두개를 이용하여 만들어 주었다. isSizeUp과 isCameraBack이 모두 true일때, 아이템을 삭제시켜준다.
각각 사이즈가 모두 줄었을때와, 카메라가 원래 위치로 돌아왔을때, true를 반환한다.

public AudioClip audioClip;
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Player"))
{
SoundManager.Instance.EffectPlay(audioClip);
Use();
ItemActive();
isContact = true;
Debug.Log("실행");
}
else if (collision.gameObject.CompareTag("Ground"))
{
StartCoroutine(ItemDestroy(5f));
}
}
public으로 AudioClip 변수를 추가해 각각의 Item에서 나오는 소리를 정할수 있게하고,
플레이어와 닿았을때 SoundManager를 통해 소리를 출력한다.

먼저 TimeItem의 경우 차량의 속도가 0일때는 충돌 처리를 하지 않게 해준다
private void OnCollisionEnter(Collision collision)
{
PlayerHealth playerHealth = collision.gameObject.GetComponent<PlayerHealth>();
if(playerHealth != null && !playerHealth.isContactVehicle && speed != 0)
{
playerHealth.HealthUpdate(contactDamage);
Vector3 contactNormal = collision.contacts[0].normal * contactPushPower;
collision.gameObject.GetComponent<Rigidbody>().AddForce(-contactNormal, ForceMode.Impulse);
SoundManager.Instance.EffectPlay(contactAudio);
StartCoroutine(playerHealth.ContactVehicle());
}
}
public bool isGiant = false;
isGiant는 현재 캐릭터의 상태를 나타낸다.
IEnumerator SizeUp()
{
Vector3 sizeVec = new Vector3(size, size, size);
playerHealth.isGiant = true;
while (player.transform.localScale.x + 0.001f <= sizeVec.x)
{
player.transform.localScale = Vector3.Lerp(player.transform.localScale, sizeVec, 0.2f);
yield return null;
}
yield return new WaitForSeconds(effectTime);
sizeVec = Vector3.one;
while(player.transform.localScale.x - 0.001f >= 1)
{
player.transform.localScale = Vector3.Lerp(player.transform.localScale, sizeVec, 0.2f);
yield return null;
}
playerHealth.isGiant = false;
isSizeUp = true;
}
크기가 커질때와 작아지고 날때에 isGiant의 변수를 변경한다.
private void OnCollisionEnter(Collision collision)
{
PlayerHealth playerHealth = collision.gameObject.GetComponent<PlayerHealth>();
if (playerHealth != null && !playerHealth.isContactVehicle && speed != 0 && !playerHealth.isGiant)
{
playerHealth.HealthUpdate(contactDamage);
Vector3 contactNormal = collision.contacts[0].normal * contactPushPower;
collision.gameObject.GetComponent<Rigidbody>().AddForce(-contactNormal, ForceMode.Impulse);
SoundManager.Instance.EffectPlay(contactAudio);
StartCoroutine(playerHealth.ContactVehicle());
}
}
TimeItem과 같이 충돌 if 조건문에 isGiant가 아닐때에만 실행되도록 한다.
protected override void Awake()
{
base.Awake();
cameraMove = FindObjectOfType<CameraMove>();
player = GameObject.FindWithTag("Player");
effectTime = 7;
oneCameraTransform = cameraMove.positionOffset;
}
만약 캐릭터의 크기가 커졌을 때, 이 아이템이 생성되고, 그 아이템을 먹게 된다면, 카메라의 위치가 비정상적으로 커지는 오류가 발생하기 때문이다.

이 버그를 수정하자면,
public Vector3 OnePositionOffset
{
get; private set;
}
private void Awake()
{
OnePositionOffset = positionOffset;
}
프로퍼티로 절대적인 카메라 원래 위치를 정해주고,
protected override void Awake()
{
base.Awake();
cameraMove = FindObjectOfType<CameraMove>();
player = GameObject.FindWithTag("Player");
effectTime = 7;
oneCameraTransform = cameraMove.OnePositionOffset;
}
IEnumerator SpeedUp()
{
playerMove.speed += 20;
yield return new WaitForSeconds(effectTime);
playerMove.speed -= 20;
StartCoroutine(ItemDestroy(0));
}
public float PlayerSpeed
{
get { return playerSpeed; }
set
{
playerSpeed = value;
if(playerSpeed >= 15)
{
playerSpeed = 15;
}
playerMove.speed += playerSpeed - playerMove.speed;
}
}
이때 SpeedItem을 먹을 상태여서 playerMove.speed의 값이 30이상 이렇게 변하게 된 상태면
playerMove.speed의 값이 음수와 같이 이상한 값으로 되어
누른 방향키의 반대쪽으로 캐릭터가 이동하게된다.
public float OneSpeed
{
get; private set;
}
private void Awake()
{
rigidBody = GetComponent<Rigidbody>();
playerInput = GetComponent<PlayerInput>();
OneSpeed = speed;
}
OneSpeed라는 프로퍼티를 만들고 Awake에서 기본값을 할당해 준다.
public float PlayerSpeed
{
get { return playerSpeed; }
set
{
playerSpeed = value;
if(playerSpeed >= 15)
{
playerSpeed = 15;
}
playerMove.speed += playerSpeed - playerMove.OneSpeed;
}
}
PlayerSpeed 프로퍼티에서 빼주는 값을 OneSpeed로 변경해준다.
public float addSpeed = 0;
생각보다 여기서 조금 헤맸는데, 해결은 PlayerMove 쪽에서 더해지는 속도값을 받는 변수를 선언함으로써 해결해 주었다. addSpeed는 TimeManager에서 추가되는 속도값을 받는다.
public float PlayerSpeed
{
get { return playerSpeed; }
set
{
playerSpeed = value;
if(playerSpeed >= 15)
{
playerSpeed = 15;
}
playerMove.speed -= playerMove.addSpeed;
playerMove.addSpeed = playerSpeed - playerMove.OneSpeed;
playerMove.speed += playerMove.addSpeed;
}
}
기존 이동속도값에 전 레벨에서 추가된 추가속도를 빼준이후, 새로운 추가속도를 다시 넣어준다.

여기까지 Item을 만들어 보았다. 아이템의 경우 다양한 효과를 내기 때문에 분량이 엄청 길어진거 같다...
떨어지는 속도, 먹었을때 변하는 체력값, 점수 증가값 등을 수정할 수 있는 데이터로 만들어준다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "Scriptable/FoodData", fileName = "Food Data")]
public class FoodData : ScriptableObject
{
public float fallSpeed;
public int increaseHealth;
public int increaseScore;
}
CreateAsseMenu를 통해 에셋을 생성하는 메뉴를 만들어 주고
ScriptableObject를 상속 받아 스크립터블 오브젝트로 사용되게끔 해준다.
변수의 경우 public 으로 떨어지는 속도, 체력증가값, 점수증가값을 두었다.
public float fallSpeed; // 떨어지는 속도
[SerializeField] int increaseHealth; // 체력 증가값
[SerializeField] int increaseScore; // 점수 증가값
[SerializeField] FoodData foodData;
[SerializeField] AudioClip eatingAudio;
ScoreManager scoreManager;
private void Awake()
{
scoreManager = FindObjectOfType<ScoreManager>();
fallSpeed = foodData.fallSpeed;
increaseHealth = foodData.increaseHealth;
increaseScore = foodData.increaseScore;
}
SerializeField로 FoodData 변수를 선언해주고 Awake에서 안의 데이터를 옮겨 받는다.






Food Data에 각각에 맞는 FoodData 에셋을 넣어준다.

Eating Audio의 경우 프라이팬과, 칼이외에는 기존 먹는 사운드를 넣어주고,
프라이팬과 칼은 따로 다른 사운드를 넣어준다.

(칼, 프라이팬 x회전 -90 xyz 스케일 4 , 호박,수박 스케일 7 , 사과 스케일 12.5 , 햄버거 스케일 17.5)
(회전값의 경우 변경이 복잡하여 적용되지 않을수도 있으니 프리팹에서 직접 수정하자...)

이후에 FoodSpawner를 약간 수정해 준다.
public IEnumerator CreateFood()
{
while(GameManager.Instance.State && isCreateTime)
{
InsFoodPosition = new Vector3(Random.Range(-foodX, foodX), foodY, Random.Range(-foodZ, foodZ)) + target.transform.position;
GameObject food = foods[Random.Range(0, foods.Length)];
Instantiate(food, InsFoodPosition, food.transform.rotation);
yield return new WaitForSeconds(TimeManager.Instance.FoodFallTime);
}
}
생성시킬 때, 회전값을 Quaternion.identity로 하게 되면 회전값(0,0,0)으로 생성되니 food.transform.rotation으로 개별적인 회전값에 맞춰 생성시킨다.
TimeManager에서 음식의 떨어지는 속도를 지정해주면 모두 같은 속도로 내려오기 때문에, TimeManager에서는 기존 음식의 떨어지는 추가적인 속도를 더해주는 방식으로 수정해줄 것이다.
private float addFoodFallSpeed = 0;
public float AddFoodFallSpeed
{
get
{
addFoodFallSpeed = FoodFallSpeed - 5;
return addFoodFallSpeed;
}
}
addFoodFallSpeed라는 변수를 통해 FoodFallSpeed의 추가된 값만 가져오게끔 되어있다.
-5 같은 경우 처음의 FoodFallSpeed 기본값으로 잡아준다.
private void Start()
{
fallSpeed += TimeManager.Instance.AddFoodFallSpeed;
}
이렇게 떨어지는 값에 TimeManager의 FoodFallSpeed가 아닌 AddFoodFallSpeed의 값을 추가해준다.


메인화면에 있는 Food 버튼을 눌렀을 때, 여러 음식들의 정보를 넣어 줄 것이다.

이 패널은 이 창이 생겼을 때, 다른 버튼을 누르는 것을 막아주는 역할을 한다.

이 패널은 불투명하게 창을 띄워 Food가 나올 창이라는 것을 인식시켜준다.

Food Image의 경우 음식의 사진을 넣어줄 공간이며 이 오브젝트를 기준으로 Food Name은 음식의 이름, Food Content는 음식의 한 줄 설명을 적을 텍스트이다.

음식 관련 정보들은 다시 Vertical LayOut Group이라는 오브젝트로 묶어주었는데, 음식의 회복량, 점수량, 속도가 동일한 간격으로 벌어지는게 좋았기 때문에 사용하였다.
그 이후에 Food Image에 들어갈 사진을 구해야 하는데, 마냥 구할 방법이 없다고 생각 했기에 직접 캡쳐를 해서 구하기로 하였다.


이렇게 하면 카메라에서 보는 화면 배경색이 검정색으로 되는 것을 볼 수 있는데, 이제 카메라를 눌러 Ctrl + Shift + F 를 눌러 각도를 정해주고 캡쳐를 해주면 된다.


지금 보면 음식의 배경이 투명이면 좋겠지만 아직 내겐 그러할 여유가 없다...

먼저 효율적인 처리를 위해 First Food Window라는 창과 Second Food Window 창을 만들어 첫번째 창에 있는 음식들과 두번째 창에 있는 오브젝트들을 분리하여 작업할 것이다.

버튼을 창 왼쪽에 둘 것이기 때문에, Second Food Window의 x위치를 오른쪽으로 살짝 옮겨주었다.

버튼은 창의 오른쪽 중간부분에 위치하고 자식오브젝트의 Text는 삭제시키고 이미지를 추가하였다.
이후 이미지에 화살표 이미지를 넣어주고 버튼의 Image 컴포넌트는 삭제 시켜주었다.

화살표의 z회전을 180도로 하여 화살표가 반대족을 향하도록 한다.

Back Ground Panel의 자식 오브젝트로 설정하여 창의 페이지가 바뀌더라도 이 버튼은 남아있다.
버튼의 이미지 컴포넌트를 삭제시키고 Text로만 '뒤로가기' 라고 작성해 주었다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FoodContent : MonoBehaviour
{
[SerializeField] GameObject foodPanel;
[SerializeField] GameObject[] foodContent;
[SerializeField] GameObject rightArrowButton;
[SerializeField] GameObject leftArrowButton;
[SerializeField] GameObject backButton;
public void Execute()
{
foodContent[0].SetActive(true);
foodContent[1].SetActive(false);
foodPanel.SetActive(true);
}
public void RightArrow()
{
foodContent[0].SetActive(false);
foodContent[1].SetActive(true);
}
public void LeftArrow()
{
foodContent[1].SetActive(false);
foodContent[0].SetActive(true);
}
public void Back()
{
foodPanel.SetActive(false);
}
}
먼저 Food Content라는 스크립트를 만들어 Food창의 로직을 만들어 준다.
foodPanel의 경우 Food 전체창, foodContent는 페이지이다.
오른쪽,왼쪽 페이지로 가는 버튼과 Food창을 닫는 버튼을 담는 변수도 만들어 준다.
함수의 경우 모두 버튼에서 호출될 것이기 때문에 public으로 선언해준다.
Execute의 경우 Food 버튼을 눌렀을때 Food창을 보여주고,
RightArrow는 오른쪽 페이지로, LeftArrow는 왼쪽페이지로 이동시켜준다.
Back은 Food의 창을 닫는다.

Food Content Manager라는 빈게임 오브젝트안에 Food Content 컴포넌트를 넣어준다.
이후 각각 변수에 알맞게 게임 오브젝트를 넣어준다. 이때 FoodContent의 순서를 주의해주자.
[SerializeField] GameObject foodButton;
public void FoodButton()
{
SceneryManager.Instance.FoodLoadScene();
}
MainSceneUIManager 스크립트에서 위 코드를 지워준다. Food의 경우 해당 스크립트에 있을 필요가 없다.
public void FoodLoadScene()
{
Debug.Log("FoodLoadScene");
}
SceneryManager 스크립트의 위 코드도 필요가 없으므로 지워주도록 하겠다.



Food 버튼,창 창에 있는 페이지를 넘기는 화살표와 뒤로가는 버튼까지 잘 작동하는 것을 볼 수 있다.