일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 팀 프로젝트
- 유니티
- 드래곤 플라이트 모작
- 오큘러스
- 유니티 UI
- 길건너 친구들
- HAPTIC
- 드래곤 플라이트
- XR
- 유니티 GUI
- 개발
- 연습
- 오브젝트 풀링
- 포트폴리오
- CGV
- meta xr
- 멀티플레이
- Photon Fusion
- meta
- OVR
- 모작
- 개발일지
- Oculus
- 가상현실
- VR
- ChatGPT
- 유니티 Json 데이터 연동
- 팀프로젝트
- 앱 배포
- input system
- Today
- Total
EasyCastleUNITY
[과제]Hero Shooter Stage1 까지 본문
Tutorial
플레이어가 목표위치에 도달하면, 문이 열리는 애니메이션과 함께, 포탈이 생성된다.
포탈에 다가가면, 화면 fadeout/in과 함께 다음 씬인 StageOne으로 이동한다.
튜토리얼 씬을 제어하는 TutorialMain이 있고, 이를 통해 제어를 한다.
TutorialMain
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using static UnityEngine.GraphicsBuffer;
public class TutorialMain : MonoBehaviour
{
[SerializeField]
private PortalGenerator portalGenerator;
[SerializeField]
private PlayerController playerController;
[SerializeField] private GameObject door;
[SerializeField] private Transform target;
[SerializeField] private GameObject fadeOut;
private Transform doorTransform;
private GameObject portal;
public GameObject Portal
{
get { return this.portal; }
}
// Start is called before the first frame update
void Start()
{
this.doorTransform = this.door.transform;
this.playerController.onPortal = () => {
Debug.Log("<color=yellow>포탈위치에 도착</color>");
//Debug.LogError("!");//error pause용
this.fadeOut.SetActive(true);
//씬전환
};
this.playerController.onTarget = () => {
Debug.Log("<color=yellow>목표위치에 도착</color>");
Destroy(target.gameObject);
if (this.door != null)
{
this.doorTransform.position = new Vector3(this.door.transform.position.x, -0.24f, this.door.transform.position.z);
this.portal = this.portalGenerator.PortalGenerate(this.doorTransform);
this.playerController.Portal = this.portal; //포탈 설정
this.door.GetComponent<DoorController>().OpenDoor();
}
};
}
}
플레이어가 타겟 위치에 도착하면, onTarget 대리자를 통해, 타겟에 도착했다는 것을 Main에 알려준다.
플레이어가 포탈 근처에 도착하면, onPortal 대리자를 통해, 포탈에 도착했다는 것을 Main에 알려준다.
포탈에 도착하면, fadeOut 게임오브젝트가 활성화되며, 화면이 fadeOut이 된다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class FadeOutMain : MonoBehaviour
{
[SerializeField] private Image dim;
private System.Action onFadeOutComplete;
// Start is called before the first frame update
void Start()
{
Image image =this.dim.GetComponent<Image>();
image.enabled = true;
this.onFadeOutComplete = () => {
SceneManager.LoadScene("StageOne");
};
this.StartCoroutine(this.FadeOut());
}
private IEnumerator FadeOut()
{
Color color = this.dim.color;
//어두어짐
while(true)
{
color.a += 0.01f;
this.dim.color = color;
if(this.dim.color.a >= 1)
{
break;
}
yield return null;
}
Debug.Log("FadeOut Complete!");
this.onFadeOutComplete();
}
}
fadeout이 종료되면 다음 씬인 StageOne으로 씬전환이 된다.
StageOne
공격을 하는 조건은 다음과 같다.
1. 몬스터가 플레이어의 인식 범위 안에 들어와, 몬스터를 인식한다.
2. 이 상태에서 이동을 하지 않으면, 일정시간마다 총알이 발사되어 공격한다.
StageOneMain
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.SceneManagement;
public class StageOneMain : MonoBehaviour
{
[SerializeField] private PlayerController player; //플레이어
[SerializeField] private BulletGenerator bulletGenerator; //총알 생성기
[SerializeField] private MonsterController monsterController1; //몬스터
[SerializeField] private MonsterController monsterController2; //몬스터
[SerializeField]
private PortalGenerator portalGenerator;
[SerializeField] private GameObject door;
private int monsterCount = 2;
// Start is called before the first frame update
void Start()
{
if (monsterController1 != null)
{
this.monsterController1.onDie = () => { //몬스터가 죽으면
Debug.LogFormat("{0} 사망", monsterController1.gameObject.name);
monsterController1.Die();
this.monsterCount--;
if (monsterCount == 0)
{
GameObject portal = this.portalGenerator.PortalGenerate(this.door.transform);
this.player.Portal = portal; //포탈 설정
this.door.GetComponent<DoorController>().OpenDoor();
}
};
}
if(monsterController2 != null)
{
this.monsterController2.onDie = () => { //몬스터가 죽으면
Debug.LogFormat("{0} 사망", monsterController2.gameObject.name);
monsterController2.Die();
this.monsterCount--;
if (monsterCount == 0)
{
GameObject portal = this.portalGenerator.PortalGenerate(this.door.transform);
this.player.Portal = portal; //포탈 설정
this.door.GetComponent<DoorController>().OpenDoor();
}
};
}
this.player.onPortal = () => {
Debug.Log("<color=yellow>포탈위치에 도착</color>");
//씬전환
SceneManager.LoadScene("StageTwo"); //아직 fadeout/in 안넣음
};
}
// Update is called once per frame
void Update()
{
if(player.H ==0 && player.V == 0) //이동하지 않으면
{
if(player.TargetMonster != null) //선택된 몬스터가 있을 시
{
bulletGenerator.BulletGenerate();
}
}
}
}
Main의 업데이트에서, 이동여부와, 타겟몬스터가 있는지 없는지 확인
MonsterController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MonsterController : MonoBehaviour
{
[SerializeField] private int hp = 5;
public System.Action onDie;
[SerializeField] private GameObject indicator;
private void OnCollisionEnter(Collision collision)
{
if (collision.collider.CompareTag("Bullet"))
{
this.hp--;
if(this.hp <= 0)
{
this.onDie();
}
}
}
public void Die()
{
Debug.Log("몬스터 사망!");
Destroy(this.gameObject);
}
public void HideIndicator()
{
Debug.Log("선택 마크 숨기기!");
this.indicator.SetActive(false);
}
public void ShowIndicator()
{
Debug.Log("선택 마크 보여주기!");
this.indicator.SetActive(true);
}
}
PlayerController
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
//몬스터 선택하는 부분까지는 완료, 공격하는 조건을 자세히 다시 설정할것
public enum eControlType
{
KeyBoard,Joystick
}
[SerializeField] private FloatingJoystick joystick; //조이스틱
[SerializeField] private eControlType controlType; //플레이어 이동 타입
[SerializeField] private float moveSpeed = 1.0f; //플레이어 이동 속도
[SerializeField] private Transform target; //튜토리얼에서의 특정위치
[SerializeField] private float radius = 1.0f; //플레이어가 자기 주변을 인식하는 반경
[SerializeField] private float rayOffsetY = 0.5f; //레이 오프셋
private GameObject portal;
private MonsterController targetMonster;
public System.Action onPortal; //튜토리얼 포탈
public System.Action onTarget; //튜토리얼 타겟
private float h = 0;
private float v = 0;
public float H
{
get { return h; }
}
public float V
{
get { return v; }
}
public GameObject Portal
{
set { this.portal = value; Debug.Log("설정완료"); }
}
public MonsterController TargetMonster
{
get { return this.targetMonster; }
}
//게임오브젝트와 거리를 저장, 범위에 들어오는 경우에 사전에 저장
Dictionary<GameObject, float> dic = new Dictionary<GameObject, float>();
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
this.Move();
this.OnTarget();
this.OnPortal();
this.SearchMonster();
}
private void Move()
{
if (this.controlType == eControlType.Joystick)
{
h = this.joystick.Direction.x;
v = this.joystick.Direction.y;
}
else if (this.controlType == eControlType.KeyBoard)
{
h = Input.GetAxisRaw("Horizontal");
v = Input.GetAxisRaw("Vertical");
}
Vector3 direction = new Vector3(h, 0, v);
float angle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;
//이동
this.transform.Translate(direction * this.moveSpeed * Time.deltaTime, Space.World);
//회전
this.transform.localRotation = Quaternion.AngleAxis(angle, Vector3.up);
}
//튜토리얼에서 특정 위치 가면 실행되는 메서드
private void OnTarget()
{
if(this.target != null)
{
float distance = Vector3.Distance(this.transform.position, target.position);
//Debug.LogFormat("distance:{0}", distance);
if (distance < 1.6f) //목표위치에 도착
{
this.onTarget(); //대리자 호출
}
}
}
//튜토리얼에서 포탈의 위치에 가면 실행되는 메서드
private void OnPortal()
{
if(this.portal != null)
{
float distance = Vector3.Distance(this.transform.position,
this.portal.transform.position);
if(distance < 4.8f)
{
//다음 씬으로 이동
this.onPortal();
}
}
}
//주변에 있는 몬스터를 검색하여 선택하는 메서드
private void SearchMonster()
{
int monsterLayerMask = 1 << LayerMask.NameToLayer("Monster");
Collider[] targetColls = Physics.OverlapSphere(this.transform.position, radius, monsterLayerMask);
this.dic.Clear(); //사전에 있는 정보 초기화
//for문을 돌며, 해당 정보를 사전에 저장, 키값은 GameObject
for(int i=0; i<targetColls.Length; i++)
{
Collider col = targetColls[i];
float distance = Vector3.Distance(col.transform.position, this.transform.position);
this.dic.Add(col.gameObject, distance);
}
//공격 사거리 내에 몬스터가 존재한다면
if(this.dic.Count > 0)
{
float nearestDistance = this.dic.Values.Min(); //값 중에 가장 작은 것을 저장
//시퀸스의 첫번째 요소를 반환하거나, 시퀸스에 요소가 없으면 기본값을 반환
//여기서는 값, 즉 거리가, nearestDistance와 같은 요소를 가져와서, 그 요소의 키 값
//즉, 게임오브젝트를 받아온다.
GameObject target = this.dic.FirstOrDefault(x => x.Value == nearestDistance).Key;
Vector3 dir = target.transform.position - this.transform.position; // 플레이어에서의 타겟의 방향
Ray ray = new Ray(this.transform.position + (Vector3.up * this.rayOffsetY), dir); //레이 생성
Debug.DrawRay(ray.origin, ray.direction * 1000f, Color.red); //레이를 씬에서 그려봄
int layerMask = 1 << LayerMask.NameToLayer("Monster") |
1 << LayerMask.NameToLayer("Wall") |
1 << LayerMask.NameToLayer("Tree");
RaycastHit hit;
if(Physics.Raycast(ray, out hit, 1000f, layerMask)) //몬스터, 벽 , 나무에 관한 정보를 검출
{
Debug.Log(hit.collider.tag);
if (hit.collider.CompareTag("Monster"))
{
if(this.targetMonster != null)
{
this.targetMonster.HideIndicator();
this.transform.LookAt(this.targetMonster.transform.position);
}
MonsterController monster = hit.collider.gameObject.GetComponent<MonsterController>();
monster.ShowIndicator();
this.targetMonster = monster;
}
else
{
MonsterController[] monsters = GameObject.FindObjectsOfType<MonsterController>();
Debug.LogFormat("몬스터의 수:{0}",monsters.Count());
foreach(MonsterController monster in monsters)
{
monster.HideIndicator();
}
this.target = null;
}
}
}
}
//주변에 몬스터가 있다면 공격하는 메서드
private void Attack()
{
}
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(this.transform.position, radius);
}
}
SearchMonster 메서드를 통해, 몬스터 찾음
몬스터가 선택되면, 밑에 선택되었다는 표식이 나타남
시연영상
전체적인 기능 구현은 완료해서, 이제 플레이어와 몬스터의 애니메이션을 수정할 생각이다.
간단하게 애니메이션을 만들어보았다.
아직 몬스터의 hit 애니메이션이 미흡하지만, 추후 고치도록 하겠다.
PlayerController - 애니메이션 추가
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
//몬스터 선택하는 부분까지는 완료, 공격하는 조건을 자세히 다시 설정할것
public enum eControlType
{
KeyBoard,Joystick
}
private enum eAnimationType
{
Idle,Run,Shoot
}
[SerializeField] private FloatingJoystick joystick; //조이스틱
[SerializeField] private eControlType controlType; //플레이어 이동 타입
[SerializeField] private float moveSpeed = 1.0f; //플레이어 이동 속도
[SerializeField] private Transform target; //튜토리얼에서의 특정위치
[SerializeField] private float radius = 1.0f; //플레이어가 자기 주변을 인식하는 반경
[SerializeField] private float rayOffsetY = 0.5f; //레이 오프셋
private GameObject portal;
private MonsterController targetMonster;
public System.Action onPortal; //튜토리얼 포탈
public System.Action onTarget; //튜토리얼 타겟
private float h = 0;
private float v = 0;
private Animator anim;
//애니메이션 파라미터 해쉬값 추출
private readonly int hashState = Animator.StringToHash("State");
public float H
{
get { return h; }
}
public float V
{
get { return v; }
}
public GameObject Portal
{
set { this.portal = value; Debug.Log("설정완료"); }
}
public MonsterController TargetMonster
{
get { return this.targetMonster; }
}
//게임오브젝트와 거리를 저장, 범위에 들어오는 경우에 사전에 저장
Dictionary<GameObject, float> dic = new Dictionary<GameObject, float>();
// Start is called before the first frame update
void Start()
{
this.anim = this.GetComponent<Animator>();
}
// Update is called once per frame
void Update()
{
this.Move();
this.OnTarget();
this.OnPortal();
this.SearchMonster();
}
private void Move()
{
if (this.controlType == eControlType.Joystick)
{
h = this.joystick.Direction.x;
v = this.joystick.Direction.y;
}
else if (this.controlType == eControlType.KeyBoard)
{
h = Input.GetAxisRaw("Horizontal");
v = Input.GetAxisRaw("Vertical");
}
Vector3 direction = new Vector3(h, 0, v);
float angle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;
//이동
this.transform.Translate(direction * this.moveSpeed * Time.deltaTime, Space.World);
//회전
this.transform.localRotation = Quaternion.AngleAxis(angle, Vector3.up);
if(this.h == 0 && this.v == 0)
{
if(this.targetMonster != null)
{
this.transform.LookAt(this.targetMonster.transform.position); //공격 할 때만 바라보도록 수정
this.anim.SetInteger(this.hashState, (int)eAnimationType.Shoot);
}
else
{
this.anim.SetInteger(this.hashState, (int)eAnimationType.Idle);
}
}
else
{
//이동 애니메이션
this.anim.SetInteger(this.hashState, (int)eAnimationType.Run);
}
}
//튜토리얼에서 특정 위치 가면 실행되는 메서드
private void OnTarget()
{
if(this.target != null)
{
float distance = Vector3.Distance(this.transform.position, target.position);
//Debug.LogFormat("distance:{0}", distance);
if (distance < 1.6f) //목표위치에 도착
{
this.onTarget(); //대리자 호출
}
}
}
//튜토리얼에서 포탈의 위치에 가면 실행되는 메서드
private void OnPortal()
{
if(this.portal != null)
{
float distance = Vector3.Distance(this.transform.position,
this.portal.transform.position);
if(distance < 4.8f)
{
//다음 씬으로 이동
this.onPortal();
}
}
}
//주변에 있는 몬스터를 검색하여 선택하는 메서드
private void SearchMonster()
{
int monsterLayerMask = 1 << LayerMask.NameToLayer("Monster");
Collider[] targetColls = Physics.OverlapSphere(this.transform.position, radius, monsterLayerMask);
this.dic.Clear(); //사전에 있는 정보 초기화
//for문을 돌며, 해당 정보를 사전에 저장, 키값은 GameObject
for(int i=0; i<targetColls.Length; i++)
{
Collider col = targetColls[i];
float distance = Vector3.Distance(col.transform.position, this.transform.position);
this.dic.Add(col.gameObject, distance);
}
//공격 사거리 내에 몬스터가 존재한다면
if(this.dic.Count > 0)
{
float nearestDistance = this.dic.Values.Min(); //값 중에 가장 작은 것을 저장
//시퀸스의 첫번째 요소를 반환하거나, 시퀸스에 요소가 없으면 기본값을 반환
//여기서는 값, 즉 거리가, nearestDistance와 같은 요소를 가져와서, 그 요소의 키 값
//즉, 게임오브젝트를 받아온다.
GameObject target = this.dic.FirstOrDefault(x => x.Value == nearestDistance).Key;
Vector3 dir = target.transform.position - this.transform.position; // 플레이어에서의 타겟의 방향
Ray ray = new Ray(this.transform.position + (Vector3.up * this.rayOffsetY), dir); //레이 생성
Debug.DrawRay(ray.origin, ray.direction * 1000f, Color.red); //레이를 씬에서 그려봄
int layerMask = 1 << LayerMask.NameToLayer("Monster") |
1 << LayerMask.NameToLayer("Wall") |
1 << LayerMask.NameToLayer("Tree");
RaycastHit hit;
if(Physics.Raycast(ray, out hit, 1000f, layerMask)) //몬스터, 벽 , 나무에 관한 정보를 검출
{
Debug.Log(hit.collider.tag);
if (hit.collider.CompareTag("Monster"))
{
if(this.targetMonster != null)
{
this.targetMonster.HideIndicator();
}
MonsterController monster = hit.collider.gameObject.GetComponent<MonsterController>();
monster.ShowIndicator();
this.targetMonster = monster;
}
else
{
MonsterController[] monsters = GameObject.FindObjectsOfType<MonsterController>();
Debug.LogFormat("몬스터의 수:{0}",monsters.Count());
foreach(MonsterController monster in monsters)
{
monster.HideIndicator();
}
this.targetMonster = null;
}
}
}
}
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(this.transform.position, radius);
}
}
MonsterController -> 애니메이션 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MonsterController : MonoBehaviour
{
private enum eMonsterState
{
Idle,Hit,Die
}
[SerializeField] private int hp = 5;
public System.Action onDie;
[SerializeField] private GameObject indicator;
private Animator anim;
private void Awake()
{
this.anim = this.GetComponent<Animator>();
}
private void OnCollisionEnter(Collision collision)
{
if (collision.collider.CompareTag("Bullet"))
{
this.hp--;
this.anim.SetInteger("MonsterState", 1); //hit //아직 미흡
if(this.hp <= 0)
{
this.Die();
}
}
}
public void Die()
{
StartCoroutine(this.CoDie());
//Destroy(this.gameObject);
}
private IEnumerator CoDie()
{
this.anim.SetInteger("MonsterState", 2); //die
yield return new WaitForSeconds(2.0f);
this.onDie();
}
public void HideIndicator()
{
Debug.Log("선택 마크 숨기기!");
this.indicator.SetActive(false);
}
public void ShowIndicator()
{
Debug.Log("선택 마크 보여주기!");
this.indicator.SetActive(true);
}
}
몬스터 hit 애니메이션이 아직 미흡하다.
'유니티 심화' 카테고리의 다른 글
SpaceShooter2D 오브젝트 풀링 응용 (0) | 2023.08.28 |
---|---|
오브젝트 풀링 연습 (0) | 2023.08.28 |
Space Shooter 간단한 UI 적용 (플레이어 Hp Bar) (0) | 2023.08.25 |
HeroShooter 씬 전환 시에 변화 (0) | 2023.08.24 |
SpaceShooter 몬스터 피격 및, 몬스터의 공격 (0) | 2023.08.24 |