Player.cs :: 총을 추가해서 해당 마우스 방향으로 총을 발사시 캐릭터가 돌아가게 하는 기능 추가
private void Turn()
{
//지정된 벡터를 향해서 회전시켜주는 함수 - 3D기능
//현재 플레이어 위치에 방향벡터를 더해서 우리가 나아가는 쪽으로 오브젝트를 회전한다는 뜻
//#1.키보드에 의한 회전
transform.LookAt(transform.position + moveVec);
//#2.마우스에 의한 회전
if (fDown)
{
//스크린 -> 월드로 Ray를 쏘는 함수
Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit rayHit;
if (Physics.Raycast(ray, out rayHit, 100))
{
Vector3 nextVec = rayHit.point - transform.position;
nextVec.y = 0; //마스크가 없기에 부피가 있는 콜라이더 물체를 클릭시 캐릭터가 빙빙 도는 현상을 막기위해 y =0 추가
transform.LookAt(transform.position + nextVec);
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
[SerializeField] float speed; //캐릭터의 속도
[SerializeField] float jumpPower; //캐릭터의 점프파워
[SerializeField] GameObject[] weapons; // 플레이어가 무기를 들고있을때 활성화 해주는 오브젝트 배열
[SerializeField] bool[] hasWeapons; // 플레이어가 해당 무기를 끼고있는것츨 체크하는 배열
//플레이어의 아이팀 소지 갯수 변수
public int ammo;
public int coin;
public int health;
public int hasGrenades;
[SerializeField] GameObject[] grenades; //수류탄들을 저장할 배열 변수, 공전 물체를 컨트롤하기 위해 배열로 생성
//아이템 소지 갯수 최대 변수
public int maxammo;
public int maxcoin;
public int maxhealth;
public int maxhasGrenades;
float hAxis; //수평선 축 value값
float vAxis; //수직선 축 value값
bool wDown = false; //Input Manager에서 추가한 Walk(Left Shift)의 키 유무 체크
bool jDown = false; //점프 키 유무 체크
bool iDown = false; //아이템,무기 획득 키 유무 체크 - 상호작용
bool fDown = false; //공격 키 유무 체크
bool isJump = false; //현재 점프 중인지 체크 유무
bool isDodge = false; //현재 회피 중인지 체크 유무
bool isSwap = false; //현재 무기 교체 중인지 체크유무 -> 교체할때 다른 액션이 간섭 못하게 하기위해
bool isFireReady = true; //현재 공격이 가능한 상태를 체크 유무
//FPS 게임 시 1, 2,3 을누르면 무기 스왑하는 기능으로 설계
bool sDown1 = false;
bool sDown2 = false;
bool sDown3 = false;
Vector3 moveVec; // hAxis와 vAxis 값을 받아와 3차원 벡터값(방향벡터) 변수
// 회피시 캐릭터 방향전환을 막기위해 생성
Vector3 dodgeVec; //회피할때 적용되는 방향벡터
Animator anim;
Rigidbody rigid;
GameObject nearGameObject; // 무기에 근접했을때, 무기 오브젝트를 저장할 변수
//GameObject equipWeapon; // 자료형 GameObject -> Weapon으로 변경
Weapon equipWeapon; // 현재 장착하고 있는 무기 오브젝트를 저장할 변수
//처음 초기값을 -1로 , 무기 첫데이터가 0번 Index이기 때문
int equipWeaponIndex = -1; // 현재 장착하고 있는 무기의 Index 변수
float fireDelay; //공격딜레이 변수
private void Awake()
{
rigid = GetComponent<Rigidbody>();
anim = GetComponentInChildren<Animator>();
}
private void Update()
{
GetInput();
Move();
Turn();
Jump();
Attack();
Dodge();
Interation();
Swap();
}
private void GetInput()
{
//해당 프로젝트 InputManager를 이용하여 만듬
//GetAxisisRaw() : Axis 값을 정수로 반환하는 함수
//GetButton() : 꾹 누르고 있을때만 호출
hAxis = Input.GetAxisRaw("Horizontal");
vAxis = Input.GetAxisRaw("Vertical");
wDown = Input.GetButton("Walk");
jDown = Input.GetButtonDown("Jump");
fDown = Input.GetButtonDown("Fire1");
iDown = Input.GetButtonDown("Interation");
sDown1 = Input.GetButtonDown("Swap1");
sDown2 = Input.GetButtonDown("Swap2");
sDown3 = Input.GetButtonDown("Swap3");
}
private void Move()
{
//정규화를 하는 이유? : 대각선 이동시 빨라지기 때문에 크기(속도)를 1로 만들어서 속도를 똑같이 만들어줘야한다.
//쉽게 말하면 방향값이 1로 보정되는 벡터라고 생각하면 된다.
//왜 빨라짐? : hAxis = 1 , vAxis = 1 인경우 대각성 이동시 피타고라스의 법칙으로 root(1^2+1^2) = root(2) = 1.141... 때문이다.
moveVec = new Vector3(hAxis, 0, vAxis).normalized;
//무기 교체시 or 무기 공격시 움직이지 못하게 설정
if (isSwap || !isFireReady) moveVec = Vector3.zero;
//회피 동작 중인경우 회피 방향벡터가 대입되어 방향전환을 못하게 막기 위한 코드
if (isDodge) moveVec = dodgeVec;
//transform 이동은 가끔 물리충돌을 무시하는 경우가 발생 함
//Rigidbody 컴포넌트에서 CollisonDetection을 Continuos로 변경
//Cpu를 더 먹어 최적화는 떨어지지만 정확도는 올라가기 때문
if (!wDown) transform.position += moveVec * speed * Time.deltaTime;
else transform.position += moveVec * speed * 0.3f * Time.deltaTime;
//SetXXXX("애니메이션 이름" , bool 변수) 를 넣어 조건문처럼 사용 할 수 있다.
anim.SetBool("isRun", moveVec != Vector3.zero);
anim.SetBool("isWalk", wDown);
}
private void Turn()
{
//지정된 벡터를 향해서 회전시켜주는 함수 - 3D기능
//현재 플레이어 위치에 방향벡터를 더해서 우리가 나아가는 쪽으로 오브젝트를 회전한다는 뜻
transform.LookAt(transform.position + moveVec);
}
//점프키와 회피키가 같지만 이동키가 눌리고 있을때는 회피, 이동 안할때는 점프를 하게된다.
private void Jump()
{
//방향벡터가 0 이기에 이동 하지 않으므로 점프
if(jDown && moveVec == Vector3.zero && !isJump && !isDodge && !isSwap) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
{
//Addforce : rigidbody에 포함된 메서드이며 해당 오브젝트에 힘을 가해주는 함수
rigid.AddForce(Vector3.up * jumpPower,ForceMode.Impulse); // Impulse : 힘을 주는 종류 중 하나로, 즉발로 힘을 준다는 의미
anim.SetBool("isJump",true);
anim.SetTrigger("doJump");
isJump = true;
}
}
private void Attack()
{
if (equipWeapon == null) //무기가 있을떄만 실행되도록 체크
{
return;
}
fireDelay += Time.deltaTime;
isFireReady = equipWeapon.rate < fireDelay; //무기에 있는 공격속도 보다 현재 딜레이가 크면 공격 가능상태로 변경
//공격키가 눌렸고 (fDonw) , 공격 준비 상태(isFireReady)고 점프 액션을 제외한 액션이 동작안할때 조건
if(fDown && isFireReady && !isSwap && !isDodge)
{
equipWeapon.Use(); //장착하고 있는 무기의 Weapon Use함수 호출
anim.SetTrigger("doSwing");
fireDelay = 0; //공격을 했기에 공격딜레이 초기화
}
}
private void Dodge()
{
//방향벡터가 0 이 아니므로 이동 하는 상태이기 때문에 회피
if (jDown && moveVec != Vector3.zero && !isJump && !isDodge && !isSwap) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
{
dodgeVec = moveVec; //회피시 현재 이동벡터를 회피벡터에 대입한뒤 Move()함수에서 대신 사용됨
speed *= 2;
anim.SetTrigger("doDodge");
isDodge = true;
Invoke("DodgeOut",0.4f);
}
}
private void DodgeOut()//회피가 끝났을때
{
speed *= 0.5f;
isDodge = false;
}
private void SwapOut()//교체가 끝났을때
{
isSwap = false;
}
private void Swap() //무기 교체
{
//무기 스왑키를 눌렀을때
//1.무기를 같은 무기로 교체 안되게 함
//2.획득하지 않은 무기는 교체를 못하게 하기위해
//이 두가지 동작을 막기위한 로직 추가
if(sDown1 && (!hasWeapons[0] || equipWeaponIndex == 0))
{
return;
}
if (sDown2 && (!hasWeapons[1] || equipWeaponIndex == 1))
{
return;
}
if (sDown3 && (!hasWeapons[2] || equipWeaponIndex == 2))
{
return;
}
//무기 인덱스를 초기화 하고 키 누르는것을 확인하고 인덱스 값 변경
int weaponIndex = -1;
if (sDown1) weaponIndex = 0;
if (sDown2) weaponIndex = 1;
if (sDown3) weaponIndex = 2;
//액션중에는 무기 변경이 안되게 조건문을 설정
if ((sDown1 || sDown2 || sDown3) && !isJump && !isDodge)
{
if (equipWeapon != null) //빈손이 아닌경우에만
equipWeapon.gameObject.SetActive(false); //빈손이 아닌경우 장착 무기 비활성화
equipWeaponIndex = weaponIndex;
equipWeapon = weapons[weaponIndex].GetComponent<Weapon>(); // 장착할 무기 저장
equipWeapon.gameObject.SetActive(true); //장착 무기 활성화
anim.SetTrigger("doSwap");
isSwap = true;
Invoke("SwapOut", 0.4f);//애니메이션이 안보여서 지연시간을 줌
}
}
private void Interation() //아이팀과 무기를 상호작용하는 하는 함수 (조작키 : E)
{
//상호작용키가 눌리고(idown) ,무기오브젝트가 비어있지 않고(nearGameObject) , 액션중에 동작이 안되게하는 조건
if(iDown && nearGameObject != null && !isJump && !isDodge)
{
if(nearGameObject.gameObject.CompareTag("Weapon"))
{
//nearGameObject 의 Item 컴포넌트를 가져와서 저장
Item item = nearGameObject.GetComponent<Item>();
int weaponIndex = item.value; //해당 아이템의 Value값을 호출
hasWeapons[weaponIndex] = true; //bool 배열에 해당 무기를 장착했다고 알림
Destroy(nearGameObject); //무기를 획득했기에 해ㅔ당 오브젝트를 파괴 -> Player내에서 파괴한게아니라 WorldSpace에서의 파괴
}
}
}
private void OnCollisionEnter(Collision collision)
{
//충돌처리 함수로 점프 후 착지 확인
if(collision.gameObject.CompareTag("Floor"))
{
anim.SetBool("isJump", false);
isJump = false;
}
}
private void OnTriggerEnter(Collider other)
{
if(other.gameObject.CompareTag("Item"))
{
//충돌된 오브젝트의 Item 컴포넌트를 호출
Item item = other.GetComponent<Item>();
//호출한 Item의 value값을 호출하여 조건문 진행
switch (item.type)
{
case Item.Type.Ammo:
ammo += item.value;
if(ammo > maxammo) ammo = maxammo;
break;
case Item.Type.Coin:
coin += item.value;
if (coin > maxcoin) coin = maxcoin;
break;
case Item.Type.Grenade:
hasGrenades += item.value;
if (hasGrenades > maxhasGrenades) hasGrenades = maxhasGrenades;
//Player가 몇개의 수류탄을 가지고 잇는지 연출 설계
//Player 주위로 수류탄이 공전하는 공전물체 생성으로 갯수 파악
grenades[hasGrenades - 1].SetActive(true);
break;
case Item.Type.Heart:
health += item.value;
if (health > maxhealth) health = maxhealth;
break;
}
//사용된 아이템 오브젝트는 파괴
Destroy(other.gameObject);
}
}
private void OnTriggerStay(Collider other)
{
//Player가 무기에 근접했다는것을 체크
if(other.gameObject.CompareTag("Weapon"))
{
//근접한 무기 오브젝트를 저장
nearGameObject = other.gameObject;
}
}
private void OnTriggerExit(Collider other)
{
//Player가 무기에서 벗어났다는것을 체크
if (other.gameObject.CompareTag("Weapon"))
{
//벗어낫기에 오브젝트를 null로 초기화
nearGameObject = null;
}
}
}
Item.cs :: 드랍되있는 아이템들은 전부 Item.cs를 따른다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Item : MonoBehaviour
{
//열거형 변수 enum
public enum Type
{
Ammo, Coin, Grenade, Heart, Weapon
};
public Type type; //아이템 종류 변수
public int value; //아이템 값을 저장할 변수 ex)회복량,총알량,무기의 코드번호 등등..
void Update()
{
//Rotate(오일러 각도) : 오브젝트를 해당 각도(방향벡터)로 회전시키는 함수
transform.Rotate(Vector3.up * 10 * Time.deltaTime);
}
}
Orbit.cs :: 물체를 타겟 주위로 공전시키는 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//수류탄이 공전하는 스크립트
public class Orbit : MonoBehaviour
{
public Transform target;
public float orbitSpeed;
Vector3 offset;
private void Start()
{
offset = transform.position - target.position;
}
void Update()
{
transform.position = target.position + offset;
//타겟주위를 회전하는 함수 , 단점 : 목표(target)이 움직이면 일그러져서 회전 반경이 이상하게 됨
//이 단점을 대처하기위해 Offset 변수 설정
transform.RotateAround(target.position,
Vector3.up,
orbitSpeed * Time.deltaTime);
//RotateAround() 후의 위치를 가지고 목표와의 거리를 유지
offset = transform.position - target.position;
}
}
Weanpon.cs :: 근접무기만 구현되있는 상태이며, 이 Weapon 스크립트는 드랍된 오브젝트가 아닌 Player 오브젝트에 자식으로 있는 무기 오브젝트들만 가지고 있다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Weapon : MonoBehaviour
{
public enum Type { Melee, Range } //근거리, 원거리
public Type type;//무기타입
public int damage;//공격력
public float rate;//공격속도
public BoxCollider meleeArea; //근접 공격범위 콜라이더
public TrailRenderer trailRenderer; //근정 공격시 연출 변수
//Player에서 호출할것이기에 public으로 접근한정자 설정
public void Use()
{
if(type == Type.Melee)
{
//똑같은 코루틴을 동작할때 Stop으로 멈추게하고 동작 해야만한다.(설계상 그렇다는거지 이걸 노리고 할수도 있음)
//비동기로 돌아가기때문에 Start로 하면 코루틴이 2개 돌아가 로직이 꼬여버릴 수 있다.
StopCoroutine(Swing());
StartCoroutine(Swing());
}
}
//열거형 함수 클래스 :: IEnumerator
IEnumerator Swing()
{
//yield 결과를 전달하는 키웓 :: yield
yield return new WaitForSeconds(0.1f);
//근접공격 콜라이더와 트레일렌더러를 활성화 시킴
meleeArea.enabled = true;
trailRenderer.enabled = true;
//근접공격을 하고 콜라이더를 바로 꺼버리면 공격이 안될 수도있으니
//지연시간을 줘서 공격 콜라이더의 수명을 늘리기
yield return new WaitForSeconds(rate);
meleeArea.enabled = false;
//근접공격의 연출을 바로 종료하지않고 지연시간을 줘서 기다리게 하고 종료
yield return new WaitForSeconds(0.1f);
trailRenderer.enabled = false;
}
}
유니티 기능 정리
1.드랍된 오브젝트(Player 몸에 장착되는 오브젝트)의 콜라이더 관리
Player 가 획득시 손에 장착되는 망치(무기) 오브젝트
1-1.빨간상자 의 Collider는 땅위에 서있어야 되기때문에 추가
RigidBody를 적용시켜 중력을 받게 해야됨
1-2.파란상자의 Collider는 Player가 충돌시 Player의 스크립트 안에 미리 무기를 저장시키는 콜라이더이다.
무기를 완전하게 획득시 해당 오브젝트의 Item 컴포넌트를 호출한다.
public class Player : MonoBehaviour
{
[SerializeField] GameObject[] weapons; // 플레이어가 무기를 들고있을때 활성화 해주는 오브젝트 배열
[SerializeField] bool[] hasWeapons; // 플레이어가 해당 무기를 끼고있는것츨 체크하는 배열
GameObject nearGameObject; // 무기에 근접했을때, 무기 오브젝트를 저장할 변수
private void Interation() //무기(item)와 상호작용하는 하는 함수 (조작키 : E)
{
//상호작용키가 눌리고(idown) ,무기오브젝트가 비어있지 않고(nearGameObject) , 액션중에 동작이 안되게하는 조건
if(iDown && nearGameObject != null && !isJump && !isDodge)
{
if(nearGameObject.gameObject.CompareTag("Weapon"))
{
//nearGameObject 의 Item 컴포넌트를 가져와서 저장
Item item = nearGameObject.GetComponent<Item>();
int weaponIndex = item.value; //해당 아이템의 Value값을 호출
hasWeapons[weaponIndex] = true; //bool 배열에 해당 무기를 장착했다고 알림
Destroy(nearGameObject); //무기를 획득했기에 해당 오브젝트를 파괴
//Player내에서 파괴한게아니라 WorldSpace에서의 파괴
}
}
}
private void OnTriggerStay(Collider other)
{
//Player가 무기에 근접했다는것을 체크
if(other.gameObject.CompareTag("Weapon"))
{
//근접한 무기 오브젝트를 저장
nearGameObject = other.gameObject;
}
}
private void OnTriggerExit(Collider other)
{
//Player가 무기에서 벗어났다는것을 체크
if (other.gameObject.CompareTag("Weapon"))
{
//벗어낫기에 오브젝트를 null로 초기화
nearGameObject = null;
}
}
}
Player.cs에서 해당되는 부분의 스크립트만 가져왔다.
OnTriggerStay/Exit 함수로 근처에 있는 무기(Item)오브젝트를 저장 및 초기화를 하고있다.
그리고 무기 오브젝트에 다가가서 <E>키를 누르면 Interation()함수가 호출되어 해당 오브젝트(무기)를 Player 오브젝트가 가지고 있다고 판단을 한다.
2.무기 오브젝트를 획득할때 스크립트로 무기를 넣어 오브젝트를 생성해줘야함?
물론, 그렇게 해도 된다. 하지만, 이번 프로젝트에서는 다른 방법으로 처리를 하였다.
우선 무기오브젝트를 장착할 때, 어떡해 하는지를 메모하겠다.
Player의 오른팔에 무기를 장착하기 위해 RightHand(자식) 으로 빈 오브젝트를 넣어 관리 할거다.
보통 3D게임에서 무기를 관리할떄 이 방법을 많이 사용하는것 같다.
우선 오른손의 무기 잡는 위치를 조정해줘야한다.
자식으로 실린더 오브젝트를 만들고 위치를 손 안으로 집어넣어서 위치 조정
손 안으로 실린더를 집어넣어 위치를 잡아주고 실린더는 빈오브젝트 처럼 보여야 되기 땜누에 Renderer 및 Collider를 비활성화 제거를 해준다.
실린더 오브젝트 -> Weapon Point 로 변경하여 무기가 위치 되는 되는곳으로 잡아준다.
해당 오브젝트 자식으로 무기 오브젝트들을 전부 자식으로 넣은뒤 비 활성화 해주고, 이 오브젝트들을 최상위 부모 클래스인 Player의 Inspector를 통해 Weapons 배열에 삽입해준다.
무기를 획득하여 Inspector에 체크가 되어있다.
1번과 2번을 통해 만들면 위 그림 처럼 완성된다.
3.공전하는 물체 오브젝트 만들기 및 유니티 에디터에 표시하기
해당 프로젝트에서 수류탄이라는 아이템을 획득시 UI 표현이 아닌 Player 캐릭터 주위로 돌리는것으로 판단하기로 했다.
수류탄 회전 위치를 표시하기 위해 아이콘을 설정하면 이렇게 표시된다.
//수류탄이 공전하는 스크립트
public class Orbit : MonoBehaviour
{
public Transform target;
public float orbitSpeed;
Vector3 offset;
private void Start()
{
offset = transform.position - target.position;
}
void Update()
{
transform.position = target.position + offset;
//타겟주위를 회전하는 함수 , 단점 : 목표(target)이 움직이면 일그러져서 회전 반경이 이상하게 됨
//이 단점을 대처하기위해 Offset 변수 설정
transform.RotateAround(target.position,
Vector3.up,
orbitSpeed * Time.deltaTime);
//RotateAround() 후의 위치를 가지고 목표와의 거리를 유지
offset = transform.position - target.position;
}
}
Orbit.cs는 RotateAround() 함수를 통해서 Player 오브젝트 주위로 y 축을 회전 시키는 스크립트다.
하지만 RotateAround()를 사용하게되면 target 오브젝트가 이동시 회전 반경이 일그러지기 때문에 따로 보정이 필요하다.
Orbit.cs를 가지고 있는 transform.position 과 target.position의 차이를 구하고 회전 반경을 구할때 다시 offset를 더해서 계속 보정하는 로직을 가지고있다.
또한 Player에서 수류탄을 획득한 갯수만큼 보여주게 하는 로직이 필요한데
public class Player : MonoBehaviour
{
public int hasGrenades;
[SerializeField] GameObject[] grenades; //수류탄들을 저장할 배열 변수, 공전 물체를 컨트롤하기 위해 배열로 생성
private void OnTriggerEnter(Collider other)
{
if(other.gameObject.CompareTag("Item"))
{
//충돌된 오브젝트의 Item 컴포넌트를 호출
Item item = other.GetComponent<Item>();
//호출한 Item의 value값을 호출하여 조건문 진행
switch (item.type)
{
case Item.Type.Grenade:
hasGrenades += item.value;
if (hasGrenades > maxhasGrenades) hasGrenades = maxhasGrenades;
//Player가 몇개의 수류탄을 가지고 잇는지 연출 설계
//Player 주위로 수류탄이 공전하는 공전물체 생성으로 갯수 파악
grenades[hasGrenades - 1].SetActive(true);
break;
}
//사용된 아이템 오브젝트는 파괴
Destroy(other.gameObject);
}
}
}
OnTriggerEnter() 아이템을 획득 할 때마다 해당 오브젝트의 수류탄 Mesh를 활성화 시켜주는 로직이다.
4.TrailRenderer 컴포넌트의 기능
TrailRenderer는 오브젝트의 잔상을 그려주는 기능이다.
player의 자식 오브젝트(근접무기)의 자식 오브젝트에 Effect로 추가하여 관리한다.
TrailRenderer 기능 설명
5.Particle System 기능 설명
여기서 설명하는 Particle System 기능은 이동시에 파티클을 생성하는 기능을 설정하는 방법이다.
(좌)Simulation Space - Local 일때, 파티클이 부모오브젝트를 따라온다. (우)Simulation Space - World 일때, 파티클이 World Space 기준으로 따라온다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
[SerializeField] float speed; //캐릭터의 속도
[SerializeField] float jumpPower; //캐릭터의 점프파워
float hAxis; //수평선 축 value값
float vAxis; //수직선 축 value값
bool wDown = false; //Input Manager에서 추가한 Walk(Left Shift)의 키 유무 체크
bool jDown = false; //점프 키 유무 체크
bool isJump = false; //현재 점프 중인지 체크 유무
bool isDodge = false; //현재 회피 중인지 체크 유무
Vector3 moveVec; // hAxis와 vAxis 값을 받아와 3차원 벡터값(방향벡터) 변수
// 회피시 캐릭터 방향전환을 막기위해 생성
Vector3 dodgeVec; //회피할때 적용되는 방향벡터
Animator anim;
Rigidbody rigid;
private void Awake()
{
rigid = GetComponent<Rigidbody>();
anim = GetComponentInChildren<Animator>();
}
private void Update()
{
GetInput();
Move();
Turn();
Jump();
Dodge();
}
private void GetInput()
{
//해당 프로젝트 InputManager를 이용하여 만듬
//GetAxisisRaw() : Axis 값을 정수로 반환하는 함수
//GetButton() : 꾹 누르고 있을때만 호출
hAxis = Input.GetAxisRaw("Horizontal");
vAxis = Input.GetAxisRaw("Vertical");
wDown = Input.GetButton("Walk");
jDown = Input.GetButtonDown("Jump");
}
private void Move()
{
//정규화를 하는 이유? : 대각선 이동시 빨라지기 때문에 크기(속도)를 1로 만들어서 속도를 똑같이 만들어줘야한다.
//쉽게 말하면 방향값이 1로 보정되는 벡터라고 생각하면 된다.
//왜 빨라짐? : hAxis = 1 , vAxis = 1 인경우 대각성 이동시 피타고라스의 법칙으로 root(1^2+1^2) = root(2) = 1.141... 때문이다.
moveVec = new Vector3(hAxis, 0, vAxis).normalized;
//회피 동작 중인경우 회피 방향벡터가 대입되어 방향전환을 못하게 막기 위한 코드
if(isDodge) moveVec = dodgeVec;
//transform 이동은 가끔 물리충돌을 무시하는 경우가 발생 함
//Rigidbody 컴포넌트에서 CollisonDetection을 Continuos로 변경
//Cpu를 더 먹어 최적화는 떨어지지만 정확도는 올라가기 때문
if (!wDown) transform.position += moveVec * speed * Time.deltaTime;
else transform.position += moveVec * speed * 0.3f * Time.deltaTime;
//SetXXXX("애니메이션 이름" , bool 변수) 를 넣어 조건문처럼 사용 할 수 있다.
anim.SetBool("isRun", moveVec != Vector3.zero);
anim.SetBool("isWalk", wDown);
}
private void Turn()
{
//지정된 벡터를 향해서 회전시켜주는 함수 - 3D기능
//현재 플레이어 위치에 방향벡터를 더해서 우리가 나아가는 쪽으로 오브젝트를 회전한다는 뜻
transform.LookAt(transform.position + moveVec);
}
//점프키와 회피키가 같지만 이동키가 눌리고 있을때는 회피, 이동 안할때는 점프를 하게된다.
private void Jump()
{
//방향벡터가 0 이기에 이동 하지 않으므로 점프
if(jDown && moveVec == Vector3.zero && !isJump && !isDodge) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
{
//Addforce : rigidbody에 포함된 메서드이며 해당 오브젝트에 힘을 가해주는 함수
rigid.AddForce(Vector3.up * jumpPower,ForceMode.Impulse); // Impulse : 힘을 주는 종류 중 하나로, 즉발로 힘을 준다는 의미
anim.SetBool("isJump",true);
anim.SetTrigger("doJump");
isJump = true;
}
}
private void Dodge()
{
//방향벡터가 0 이 아니므로 이동 하는 상태이기 때문에 회피
if (jDown && moveVec != Vector3.zero && !isJump && !isDodge) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
{
dodgeVec = moveVec; //회피시 현재 이동벡터를 회피벡터에 대입한뒤 Move()함수에서 대신 사용됨
speed *= 2;
anim.SetTrigger("doDodge");
isDodge = true;
Invoke("DodgeOut",0.4f);
}
}
private void DodgeOut()//회피가 끝났을때
{
speed *= 0.5f;
isDodge = false;
}
private void OnCollisionEnter(Collision collision)
{
//충돌처리 함수로 점프 후 착지 확인
if(collision.gameObject.CompareTag("Floor"))
{
anim.SetBool("isJump", false);
isJump = false;
}
}
}
Follow.cs :: 카메라가 등록한 Target(오브젝트)를 따라다니는 기능
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//카메라가 플레이어를 따라가는 기능
public class Follow : MonoBehaviour
{
public Transform target; // 따라갈 오브젝트
public Vector3 offset; // 위치 오프셋(카메라를 배치할 위치 )
// Update is called once per frame
void Update()
{
transform.position = target.position + offset;
}
}
Item.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Item : MonoBehaviour
{
//열거형 변수 enum
public enum Type
{
Ammo, Coin, Grenade, Heart, Weapon
};
public Type type; //아이템 종류 변수
public int value; //아이템 값을 저장할 변수
}
1.정규화(Nomarlize)를 하는이유
벡터는 크기와 방향값을 둘다 가지는 단위이다.
간단히 말하면 정규화라는 것은 해당 벡터의 크기를 1로 변환하여 순수한 방향값을 얻을때 주로 사용된다.
//정규화를 하는 이유? : 대각선 이동시 빨라지기 때문에 크기(속도)를 1로 만들어서 속도를 똑같이 만들어줘야한다.
//쉽게 말하면 방향값이 1로 보정되는 벡터라고 생각하면 된다.
//왜 빨라짐? : hAxis = 1 , vAxis = 1 인경우 대각성 이동시 피타고라스의 법칙으로 root(1^2+1^2) = root(2) = 1.141... 때문이다.
moveVec = new Vector3(hAxis, 0, vAxis).normalized;
해당 코드에서 대각선으로 이동시 캐릭터가 더 빠르게 움직이게 된다.
피타고라스 법칙으로 hValue = 1, vValue =1 인경우 대각선 이동시 1.141.. 로 빠르게 이동된다.
그렇기 때문에 해당 벡터의 크기를 1로 정규화를 하면 순수한 방향벡터값을 구할 수 있다.
1시방향으로 대각선 이동 - 방향벡터1시방향으로 대각선 이동 - 정규화 된 방향벡터
2. 하나의 키로 두가지 동작하기 (점프와 회피) + 회피시 방향전환 못하게 막기
bool isDodge = false; //현재 회피 중인지 체크 유무
Vector3 moveVec; // hAxis와 vAxis 값을 받아와 3차원 벡터값(방향벡터) 변수
// 회피시 캐릭터 방향전환을 막기위해 생성
Vector3 dodgeVec; //회피할때 적용되는 방향벡터
private void GetInput()
{
//해당 프로젝트 InputManager를 이용하여 만듬
//GetAxisisRaw() : Axis 값을 정수로 반환하는 함수
//GetButton() : 꾹 누르고 있을때만 호출
hAxis = Input.GetAxisRaw("Horizontal");
vAxis = Input.GetAxisRaw("Vertical");
wDown = Input.GetButton("Walk");
jDown = Input.GetButtonDown("Jump");
}
private void Move()
{
////////
moveVec = new Vector3(hAxis, 0, vAxis).normalized;
Debug.Log("정규화된 벡터 : " + moveVec);
//회피 동작 중인경우 회피 방향벡터가 대입되어 방향전환을 못하게 막기 위한 코드
if (isDodge) moveVec = dodgeVec;
///////
}
//점프키와 회피키가 같지만 이동키가 눌리고 있을때는 회피, 이동 안할때는 점프를 하게된다.
private void Jump()
{
//방향벡터가 0 이기에 이동 하지 않으므로 점프
if(jDown && moveVec == Vector3.zero && !isJump && !isDodge) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
{
//Addforce : rigidbody에 포함된 메서드이며 해당 오브젝트에 힘을 가해주는 함수
rigid.AddForce(Vector3.up * jumpPower,ForceMode.Impulse); // Impulse : 힘을 주는 종류 중 하나로, 즉발로 힘을 준다는 의미
anim.SetBool("isJump",true);
anim.SetTrigger("doJump");
isJump = true;
}
}
private void Dodge()
{
//방향벡터가 0 이 아니므로 이동 하는 상태이기 때문에 회피
if (jDown && moveVec != Vector3.zero && !isJump && !isDodge) //액션 도중에 다른 액션이 실행되지 않도록 조건 추가
{
dodgeVec = moveVec; //회피시 현재 이동벡터를 회피벡터에 대입한뒤 Move()함수에서 대신 사용됨
speed *= 2;
anim.SetTrigger("doDodge");
isDodge = true;
Invoke("DodgeOut",0.4f);
}
}
Player.cs에 있는 코드를 가져왔다.
*하나의 키로 두가지 동작하기 (점프와 회피)
해당 프로젝트는 Player의 점프 키와 회피키를 같은것을 사용하고 있다.
if로 조건문을 걸어 각 액션동작 중에 다른 동작을 하지 못하게 막아놨다.
moveVec가 0이라는 것은 GetInput() 함수에서 키 입력이 없기에 0을 리턴한것이고
즉, 이동하지 않았다는 것이기 때문에 moveVec == Vector3.zero 로 조건을 걸어 각 액션시 동작을 다르게 하였다.
* 회피시 방향전환 못하게 막기
moveVec가 아닌 회피할때만 사용하는 방향벡터를 만들면 해결되는 문제이기에
dodgeVec를 생성하고 회피행동시 dodgeVec = moveVec를 선언하고
현재 회피중 일때 (isdodge 가 true) moveVec 를 계속 dodgeVec로 초기화해주는 것이다.
3.이번 프로젝트에서 사용된 모르는 함수들 정리
GetAxisisRaw() : Axis 값을 정수로 반환하는 함수
GetButton() : 꾹 누르고 있을때만 호출
ex)
hAxis = Input.GetAxisRaw("Horizontal");
vAxis = Input.GetAxisRaw("Vertical");
wDown = Input.GetButton("Walk");
jDown = Input.GetButtonDown("Jump");
/*=========================================================*/
SetXXXX("애니메이션 이름" , bool 변수) 를 넣어 조건문처럼 사용 할 수 있다.
ex)
anim.SetBool("isRun", moveVec != Vector3.zero);
anim.SetBool("isWalk", wDown);
/*=========================================================*/
transform.LookAt(Vector3 vec) : 지정된 벡터를 향해서 회전시켜주는 함수 - 3D기능
ex)
//현재 플레이어 위치에 방향벡터를 더해서 우리가 나아가는 쪽으로 오브젝트를 회전한다는 뜻
transform.LookAt(transform.position + moveVec);
/*=========================================================*/
Addforce : rigidbody에 포함된 메서드이며 해당 오브젝트에 힘을 가해주는 함수
ex)
// Impulse : 힘을 주는 종류 중 하나로, 즉발로 힘을 준다는 의미
rigid.AddForce(Vector3.up * jumpPower,ForceMode.Impulse);
Unity 기능 살펴보기
1.InputManager 제어하기
현재는 InputSystem을 사용하여 프로젝트를 개발한다는데 기회가되면 InputSystem을 사용해봐야겠다.
InputSystem을 사용하면 크로스플랫폼 개발에 큰 도움을 준다.
2.테스트 중 캐릭터의 점프가 우주에서 하는것처럼 느껴질때 대처
오브젝트의 RigidBody 를 건드는것이 아닌 ProjectSetting의 중력 값을 조절하면된다.
y값이 작아질수록 중력이 쎄진다.
3.캐릭터가 벽과 충돌할때 벽에 끼이면서 느리게 떨어지는거 대처
벽을 뚫는거는 잠시 무시하자 , 집중해야되는 건 벽에서 내려올때 끼이는것이 문제
해당 문제를 해결하기 위해 3가지 방법을 적용해야한다..
3-1. Rigidbody의 Collision Detection > Continuos는 Static과 충돌할 때 효과적
3-1-1.transform 이동은 가끔 물리충돌을 무시하는 경우가 발생 함 ,
Rigidbody 컴포넌트에서 CollisonDetection을 Continuos로 변경하게 되면 계속 충돌처리를 탐색하게 된다.
이렇게 되면 Cpu 처리비용을 더 먹어 최적화는 떨어지지만정확도는 올라가기 때문이다.
3-2.서로 두 오브젝트가 충돌할때 유니티가 효과적으로 계산하기 위해서는 두 오브젝트다 Rigidbody를 가지고 있어야함
3-2-1.고정 오브젝트인 경우 is Kinematic을 체크하고 Use Gravity를 체크해제 해주면됨
3.점프하면서 벽에 비벼지며 느리게 떨어지는 것은 Physic Material을 이용하여 마찰력이 없는 벽을 만들어 Collider에 적용
Physic Material은 물리가 적용되는 재질로 Collider에 적용된다.
이럼 이제 벽에 점프해서 충돌할때 매끄럽게 내려오는 모습을 볼 수 있다.
4.Animater의 Parameter 결정 조건
현재 프로젝트에 적용되고있는 Player - MeshObject의 Animator
Roop Animation들은 보통 bool 파라미터를 사용한다.
일회성 애니메이션(점프,회피)은 Trigger 파라미터를 사용한다.
Trigger 파라미터는 doXXX로 작성 하였고 스크립트에서 On을 하면 애니메이션이 재생되고
애니메이션이 끝난 후 Off하게 되는 파라미터이다.
5.3D에 오브젝트(부모,자식)배치하기 - 하이어라키(계층 구조의 이해)
오브젝트의 위치는 그대로고 자식오브젝트인 MeshObject만 조절하여 땅에서 올리고 회전시킨 모습
부모오브젝트는 (0,0,0) 위치에 배치하고 MeshObject의 위치(0,1.7,0)만 조절하여 땅위로 올린 모습이다.
플레이어 캐릭터의 하이어라키 구조
모든 프로젝트가 이런건 아니지만 3D에서는 Sprite가 아닌 Mesh들을 가지고 3D 오브젝트를 관리하기에
내 나름대로 정리하는게 좋을것 같아서 정리 하였다.
1.Player의 최상위 부모에 Rigid,Collider를 넣어서 물리현상 구현 및 Player 스크립트 동작을 하게 하였다.
2.Mesh Object는 캐릭터의 모습을 보여주는 기능이기에 따로 자식으로 뺸뒤 Animator 컴포넌트를 추가하여 캐릭터의 모습에 애니메이션을 추가하는 기능까지 부여하였고
3.기능적인건은 따로 자식오브젝트로 뺼 수도 잇고(무기를 든다거나,스킬을 가지고 있다거나 등등)
즉, 프로젝트 설계에 따라 부모 -자식 및 기능 적으로 분리를 잘 해야 프로젝트가 수월해진다.
6. Object에 Light 추가하기 + Particle
아이템에 Light , Particle 빈 오브젝트를 만든뒤 각각 Light , Particle System컴포넌트를 추가하였다.
Transforn . y 값 조절 : 위치를 조절하여 빛의 위치 조정
Light - Intensity : 빛의 세기 조절
Light - Range : 빛의 범위 조절
마지막 움짤을 보면 아이템 밑에 부위에도 빛이 비치는 장면이 있다.
*Particle System 간단 설명
Particle System의 Particle 카테고리위에 설명한 기능들을 적용하는 모습
Particle System - Renderer 설정
Material을 변경하여 파티클 이미지를 변경 할 수 있다.
Particle System - Emission 기능
Emission(방출)은 파티클의 갯수에 영향을 준다. Time의 숫자가 커질수록 파티클의 갯수가 증가한다.
ParticleSystem-Shape 기능
Shape 기능은 파티클이 어떤 방식으로 뻗어 나갈건지 결정하는 기능이다. Corn 모양일때 원뿔 모양에서 파티클이 뿜어져 나왔고 Donut일때는 사방으로 퍼져 나오는 모습을 볼 수있으며 퍼져나오는 근원의 위치,각도,크기를 조절 할 수 있다.
ParticleSystem-Color over LifeTime 기능
Color over LifeTime 은 파티클의 색상을 결정하는 기능이다. 해당 이미지를 보면 이해가 될것이다.
색상 변경을 하여 위로 올라갈수록 색이 변경되는 모습을 볼 수있다.ParticleSystem-Size over LifeTime 기능
Size over LifeTime 은 파티클이 살아있는동안 크기를 변화하는 기능이다.
Unity에서 제공하는 프리셋이 있어 그걸 사용해도 된다. 위의 이미지 처럼 위로 올라갈수록 파티클의 크기가 커지는 모습을 볼 수있다.
씨네머신은 Scene에 여러 카메라를 관리하고 쉽게 카메라 법칙을 추가 할수 있는 Unity에서 지원하는 강력한 패키지 이다.
*오브젝트를 따라가는 간단한 카메라 설정하기 - Cinemachine
MainCamera 오브젝트에 CinemachineBrain 컴포넌트를 추가한다.
[GameObject] - [Cinemachin] - [VirtualCamera]를 통해 가상 카메라 오브젝트를 생성한다.
Follow에 오브젝트(노란상자)를 등록하면 해당 오브젝트를 계속 따라다니면서 촬영하게 된다.Look At에 오브젝트(노란상자)를 따라가진 않지만 계속 오브젝트 방향으로 촬영을 한다.Follow : 노란상자 오브젝트 / Look At : 파란색 긴 기둥 인경우 이런 촬영도 가능하다.
유니티 5주차 - 하이퍼캐주얼 게임 개발에서 터치패드를 구현할떄 사용 하였다.해당 게임에서 구현한 옵저버 패턴
나는 옵저버패턴을 이런식으로 구현해서 사용했지만 , 실제로는 이렇게 구현하면 안된다.
일대다관계로 구성이 되야하지만 현재 일대일 관계로 2개를 구현하였다.
다음에는 구현 시 주의하면 만들어봐야겠다.
C# - Delegate Vs Envet ?
C++ 의 함수포인터를 C#으로 변환한게 Delegate이다.
Delegate는 멀티캐스팅 기능으로 하나의 델리게이트로 여러 인스턴스(객체)가 여러 메서드(함수)를 호출 할수 있도록 구성이 가능하다.
하지만 이렇게되면 객체지향의 4대원칙 중 보안성이 지양되기 때문에 Delegate가 아닌 Event 함수가 주로 사용됩니다.
Event함수는 Delegate기반으로 되있지만 엑세스 제한으로 외부에서 이벤트를 추가(+=)/ 제거(-=)만 가능하며 이벤트 핸들러에 등록된 함수가 호출되지 않는다.
즉, Event함수의 함수호출은 내부클래스에서만 호출이 된다.
특징
Delegate
Event
역할
함수포인터로 사용
델리게이트에 대한 접근을 제한하고 이벤트 모델 제공
엑세스 제어
어디서든 호출 가능
선언된 클래스 외부에서는 호출불가
사용 목적
일반적인 콜백이나 함수 참조 관리
안전한 이벤트 발생 및 처리
기본 엑세스
public
특별히 제한된 엑세스 (add/Remove만 허용)
멀티캐스트 지원
가능
가능
<이 부분은 개인적으로 생각한 것을 적은 부분입니다. 틀린 내용이 있으시면 댓글 부탁드립니다 >
왜 하나의 클래스에 Delegate와 Event를 동시에 쓰나요?
Delegate는 함수 포인터라는 자료형의 변수를 선언하는 것과 유사하다.
그리고 Event는 Delegate에서 선언한 변수를 자료형으로 삼고 사용한다.
public delegate void TouchPadMoveHandler(); // delegate 키워드를 사용하여 함수포인터 변수 선언
public static event TouchPadMoveHandler OnTouchLeftPad; //선언한 변수를 자료형으로 event 함수 선언
각 블럭의 높이를 줄여 공간을 확보 하였고 , 가시의 갯수가 많아 게임이 너무 어려워 1개로 듬성듬성 배치하기로 함
2.블럭 끼리 겹치지 않게 구현 - BlockSpanwer에서 생성 로직 변경 필요
코드를 따로 변경하진 않았고, 블럭의 속도를 전부 똑같이 통일 하였다.
전부 똑같이 변경한 이유는 게임이 [점프킹] 같은 점프 시스템을 가져왔다보니
블럭들의 속도가 종류에 상관없이 일정해야 게임의 난이도가 좀 나아질것 같다.
3. LV에 따른 스테이지 난이도 변경
BlockSpanwer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum defineNum
{
Blokc_End_posX = -17, //해당 x좌표에 닿으면 pivot이 닿으면 사라지는 좌표
}
public class BlockSpawner : MonoBehaviour
{
[SerializeField] List<Block> block_list;
[SerializeField] float blockSummonCoolTime = 2.0f;
[SerializeField] float startPosX = 17.0f; //생성 시작되는 위치 x좌표
[SerializeField] float startPosY_Min = -5.0f;
[SerializeField] float startPosY_Max = 2.0f;
Coroutine coroutineBlockSpanw;
[SerializeField] List<float> blockSpawnPosY;
[SerializeField] bool isstop = false;
int isSpawnBlockIndex = -1;
public void BlockSpawnerON()
{
if (!isstop)
{
if (coroutineBlockSpanw == null)
{
coroutineBlockSpanw = StartCoroutine(BlockSpanw());
}
else
{
StopCoroutine(coroutineBlockSpanw);
coroutineBlockSpanw = StartCoroutine(BlockSpanw());
}
}
}
public void BlockSpawnerOFF()
{
if (coroutineBlockSpanw != null)
{
StopCoroutine(coroutineBlockSpanw);
}
}
IEnumerator BlockSpanw()
{
float LevelSuummonCoolTime;
while (true)
{
MakeBlock();
//현재 레벨의 10% 만큼 블럭 생성주기 증가 -> 블럭이 늦게 등장함
LevelSuummonCoolTime = blockSummonCoolTime + ( (GameManager.Instance.CurLevel - 1) * 0.1f);
yield return new WaitForSeconds(LevelSuummonCoolTime);
}
}
private void MakeBlock()
{
//블록 생성확률이 100% 에서 75%까지 떨어짐
//생성확률은 6레벨 이하까지만 적용 그 이후로는 50% 동일
int curStageLv = GameManager.Instance.CurLevel;
int SuumonPercent = curStageLv >= 5 ? 5 : (curStageLv - 1);
if (SuumonPercent <= Random.Range(0, 20))
{
int type = Random.Range(0, block_list.Count);
Block block = Instantiate(block_list[type]);
block.gameObject.SetActive(false);
float x = startPosX;
//y 범위 : -4.5 ,-1.5,1.5, 4.5
//한번 생겼던 위치에 다시 생기지 않는 함수
int y;
do
{
y = Random.Range(0, blockSpawnPosY.Count);
}
while (isSpawnBlockIndex == y);
isSpawnBlockIndex = y;
block.Block_pos = new Vector3(x, blockSpawnPosY[y], 0);
//레벨 수치의 10% 만큼 모든 블럭의 speed 증가
if (curStageLv > 1) block.Speed += (curStageLv * 0.1f);
block.gameObject.SetActive(true);
}
}
}
블럭 생성시 게임매니저에서 현재 스테이지 레벨을 호출 하여 블록 생성 확률,블록 생성 주기,블록 이동 속도수치를 조절하게 함.
4. 블럭에 빠지는 함정 만들기(함정 특징: 땅바닥인척 하지만 갑자기 내려감)
1.좌우로 이동하는 블록 추가
BlockGroup.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BlockGroup : Block
{
[SerializeField] protected List<GameObject> blocks;
private void Update()
{
MoveBlock();
}
}
MovePattern.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MovePattern : MonoBehaviour
{
[SerializeField] float avoidSpeed = 2.0f;
int dir = 1;
private Vector3 originPos;
private float moveRange = 1.0f;
private void Start()
{
//처음 위치 좌표 저장
originPos = transform.localPosition;
}
private void Update()
{
//이동거리(MoveRange) 이상 이동한경우 방향값(dir) 변경
if(Mathf.Abs(transform.localPosition.x - originPos.x) >= moveRange) dir *= -1;
transform.localPosition += dir * ((Vector3.right * avoidSpeed)) * Time.deltaTime;
}
}
좌,우로 왔다 갔다 하는 패턴을 만들때 왼쪽/오른쪽 좌표로 설정해도 되지만
조건문을 이동거리(MoveRange)로 사용하면 좌표 값을 건드리지 않고 이동거리로 제어가 가능해져서 유용하다.
2.밟으면 떨어지는 블록 추가
DropPattern.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DropPattern : MonoBehaviour
{
[SerializeField] float DropSpeed = 0.5f;
private void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.CompareTag("Player"))
{
StartCoroutine(Drop());
}
}
IEnumerator Drop()
{
while(transform.position.y >= -12f)
{
transform.position += Vector3.down * DropSpeed * Time.deltaTime;
yield return null;
}
}
}
위에 블럭들의 콜리더 분리가 필요해서 부모 오브젝트를 만들고 자식으로 3개의 블럭을 추가하였다.
각 블럭마다 콜리더를 추가하였고 기믹이 필요한곳에만 스크립트(MovePattern.cs , DropPattern.cs)를 넣어서 사용 하였고 3개의 블럭은 동시에 같은속도로 같이 이동해야 되기때문에
새 스크립트인 BlockGroup 스크립트를 만들고 Block을 부모로 상속하여 이동하는 함수를 사용하였다.
4-1. Player 점프 로직 변경(떨어지는 블럭 위에 있을때 점프 가능하게)
떨어지는 블럭에 캐릭터가 서있으면 OnColliderEnter() 메서드가 자꾸 호출하여서 점프가 안되는 버그가 있었다.
Collider 함수 안에 점프상태를 초기화하는 내용이 있음
대처방법으로 콜리더가 호출 되더라도 점프 상태인 경우에만 점프상태를 초기화 할 수 있게 로직을 변경하였음
떨어지지만 게이지는 초기화 되지 않기 때문에 점프가 가능해짐
Player.cs
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.CompareTag("Block"))
{
// 캐릭터가 점프 + 점프중 이라는 상태를 만족해야만 멈춤
if (isJump && isJumping)
{
PlayerStop();
anim.SetBool("isJump", false);
}
}
if (collision.gameObject.CompareTag("Spike"))
{
Debug.Log("Player 가시 닿음");
isdead = true;
anim.SetBool("isDead", true);
PlayerStop();
GameManager.Instance.PlayerDie();
}
}
이유 : 게임을 플레이 해보는 도중 경계선에 있으면 캐릭터가 화면 밖에 있을때 조작이 잘 되고있는지 못하는 불편함을 느껴 추가하기로 하였다.
하지만 카메라가 오브젝트를 추적 할 수있는 메서드를 몰라 구글링 및 ChatGpt의 도움을 받아 추천 받았다.
카메라 안에 있는 특정 오브젝트를 찾는 방법은 2가지를 알았다.
1-1.첫번째는 Camera.WorldToViewportPoint' 메서드를 사용
해당 메서드는 월드 좌표를 뷰 포트 좌표로 변환해주는 역할을 한다.
ViewPort란 좌측하단(0,0) 에서 우측상단(1,1)을 가르키는 좌표계이다. 최소(-1,-1) 에서 최대 (2,2)까지 값을 가진다.
즉,카메라가 보는 영역인 ViewPort 좌표계에 Player 월드좌표를 변환 시켜서 사용 하는것이다.
public float originSize = 6.87f;
public float targetSize = 15f; // 목표로 하는 Orthographic Size
public float zoomSpeed = 5f; // 줌 인/아웃 속도
void ObjINCamera() //해당 메서드를 Update()에 포함 시킴
{
// 오브젝트의 월드 좌표를 뷰포트 좌표로 변환
Vector3 viewportPos = Camera.main.WorldToViewportPoint(transform.position);
// 뷰포트 좌표가 카메라의 시야에 있는지 확인
if (!(viewportPos.x >= 0 && viewportPos.x <= 1 && viewportPos.y >= 0 && viewportPos.y <= 1 && viewportPos.z > 0))
{
Debug.Log("오브젝트는 카메라의 뷰 밖에 있습니다.");
Camera.main.orthographicSize = Mathf.Lerp(Camera.main.orthographicSize, targetSize, Time.deltaTime * zoomSpeed);
}
else
{
Debug.Log("오브젝트는 카메라의 뷰 안에 있습니다.");
Camera.main.orthographicSize = Mathf.Lerp(Camera.main.orthographicSize, originSize, Time.deltaTime * zoomSpeed);
}
}
이런식으로 하면 Lerp 함수를 통하여 자연스럽게 화면이 Zoom기능이 구현된다.
1-2.GeometryUtility.TestPlanesAABB 메서드를 사용한 방법
GeometryUtility.CalculateFrustumPlanes 와 GeometryUtility.TestPlanesAABB 두 메서드를 사용하는 방법이다.
카메라의 View frustum(절두체)에 오브젝트의 바운딩 박스가 포함되는지 확인 하는 방법이다.
절두체는 카메라가 실질적으로 비추는 3D 공간이라 생각하면된다.
즉, 절두체에 오브젝트가 존재하는지 체크하여 Zoom 기능이 동작하는 코드이다.
using UnityEngine;
public class ObjectInCameraView : MonoBehaviour
{
public Camera mainCamera; // 사용할 카메라
void Update()
{
// 카메라의 뷰 프러스텀을 계산
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(mainCamera);
// 오브젝트의 바운딩 박스를 얻음
Bounds objectBounds = GetComponent<Renderer>().bounds;
// 오브젝트의 바운딩 박스가 카메라의 뷰 프러스텀 안에 있는지 확인
if (GeometryUtility.TestPlanesAABB(planes, objectBounds))
{
Debug.Log("오브젝트는 카메라의 뷰 안에 있습니다.");
}
else
{
Debug.Log("오브젝트는 카메라의 뷰 밖에 있습니다.");
}
}
}
절두체는 컬링기법(그래픽 렌더링을 줄여 최적화하는 기법중 하나)을 사용할떄 주로 나타나는 이론이다.