//총알을 180도로 아래 방향으로 총알의 방향벡터를 만드는 로직
void MakeCircle()
{
Vector3 targetPos = playerTransform.position;
Vector3 myPos = new Vector3(0f, 1.4f, 0);
Vector3 vectorToTarget = (targetPos - myPos).normalized;
int n = 30;
for(int i = 0; i < n; i ++)
{
GameObject MakeCircle = Instantiate(bullet, myPos, Quaternion.identity);
Vector2 circleVec = new Vector2(Mathf.Cos((Mathf.PI) * i / n ), Mathf.Sin(-(Mathf.PI) * i / n ));
MakeCircle.GetComponent<Rigidbody2D>().AddForce(circleVec * 30f);
}
}
삼각함수를 통해서 원하는 각도의 좌표방향을 알수있다.
Mathf.Cos()은 x축 방향을 의미, Mathf.Sin()은 y축 방향을 의미한다.
단위 원 일때 (반지름이 1인 원) Cos = 밑변 / 빚면인데 빚면은 반지름을 나타내고 결국 밑변은 x좌표값을 나타낸다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public struct Stage
{
public bool isBoss;
public int stageNum;
public int boardWidth;
public int boardHeight;
public bool isClear;
}
Stage 관리를 하기위해 Stage 구조체를 만들었다.
public class DataManager : MonoBehaviour
{
[SerializeField] private List<Stage> stages;
private int curStage;
public List<Stage> Stages { get { return stages; } }
public int CurStage { set { curStage = value - 1; } }
public Stage GetCurStgae()
{
return stages[curStage];
}
public void SetCurStgaeIsClear()
{
//해당 코드는 문법상 오류 발생
//stages[curStage].isClear = false;
//구조체 변수를 새로 선언해서 초기화 해줘야함
Stage stage = stages[curStage];
stage.isClear = true;
stages[curStage] = stage;
}
Class 내에 Stage 구조체를 이용해 List를 만들어서 List[index]로 변수에 접근했으나 Stage의 변수에 접근하지 못했다.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using UnityEngine;
[ExcelAsset]
public class DialogDB : ScriptableObject
{
public List<DialogDBEntity> Stage1; // Replace 'EntityType' to an actual type that is serializable.
public List<DialogDBEntity> Stage2; // Replace 'EntityType' to an actual type that is serializable.
//외부에서 내부 변수를 이름으로 호출 할 수있는 함수
public object PrintField(string name)
{
// public변수가 아니면 GetField에서 null이 리턴된다
var result = this.GetType().GetField(name).GetValue(this);
return result;
}
}
DialogeSystem.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
public class DialogeSystem : MonoBehaviour
{
[SerializeField] DialogDB dialogDB;
List<DialogDBEntity> curStageDialoge;
private void Start()
{
//해당 오브젝트가 활성화시 Stage1의 DialogDB를 호출한다.
DialogLoad("Stage1");
}
public void DialogLoad(string StageName)
{
//매개변수의 StageName을 받아서 dialgoDB Class의 변수를 이름으로 호출한다.
object curStage = dialogDB.PrintField(StageName);
//object 자료형으로 호출하였기에 언박싱을 하여 원래의 자료형으로 돌려줘야한다.
curStageDialoge = (List<DialogDBEntity>)curStage;
}
}
DialogeSystem 에서 DialogeDB의 PrintField() 메서드를 통해서 해당 클래스의 변수를 박싱( List<DialogDBEntity ->var)해서 받아오고, 받아온 변수를 언박싱(object -> List<DialogDBEntity>)하여 내가 쓸수있게 만들어준다.
1.오브젝트와 충돌시 회전속력이 발생하는 버그를 없애기위해 FreezeRotation()함수 추가
2.벽을 관통하는 버그가 있어 StopToWall() 함수 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
bool isBorder = false; //현재 벽과 충돌했는지 체크 유무
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 무기 공격시 or 재장전시 움직이지 못하게 설정
if (isSwap || !isFireReady || isReload) moveVec = Vector3.zero;
//회피 동작 중인경우 회피 방향벡터가 대입되어 방향전환을 못하게 막기 위한 코드
if (isDodge) moveVec = dodgeVec;
//transform 이동은 가끔 물리충돌을 무시하는 경우가 발생 함
//Rigidbody 컴포넌트에서 CollisonDetection을 Continuos로 변경
//Cpu를 더 먹어 최적화는 떨어지지만 정확도는 올라가기 때문
//위의 조건과 같이 걸어버리면 회전할시에 moveVec에 영향을 받게 되서 캐릭터가 굳어버릴수 있다.
if (!isBorder)
{
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기능
//현재 플레이어 위치에 방향벡터를 더해서 우리가 나아가는 쪽으로 오브젝트를 회전한다는 뜻
//#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);
}
}
}
void FreezeRotation()
{
//rigid.angularVelocity : 물리회전속도
rigid.angularVelocity = Vector3.zero;
}
void StopToWall()
{
Debug.DrawRay(transform.position, transform.forward * 5, Color.green);
//레이어마스크(Wall)과 5 거리 안에 충돌시 isBorder는 true가 될것이다.
isBorder = Physics.Raycast(transform.position, transform.forward, 5, LayerMask.GetMask("Wall"));
}
private void FixedUpdate()
{
FreezeRotation();
StopToWall();
}
}
Item.cs :: 드랍 아이템들의 Player와의 물리충돌을 없애기 위해
1.Floor와 충돌시 Rigidbody를 Kinematic 처리 와 물리충돌을 담당하는 Collider을 비활성화
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class Item : MonoBehaviour
{
Rigidbody rigid;
SphereCollider sphereCollider;
//열거형 변수 enum
public enum Type
{
Ammo, Coin, Grenade, Heart, Weapon
};
public Type type; //아이템 종류 변수
public int value; //아이템 값을 저장할 변수
private void Awake()
{
rigid = GetComponent<Rigidbody>();
//Item 오브젝트에는 2개의 스피어콜라이더가 있음
//GetComponent 호출시 Inspector 제일 위에있는 컴포넌트를 호출하게됨
sphereCollider = GetComponent<SphereCollider>();
}
void Update()
{
//Rotate(오일러 각도) : 오브젝트를 해당 각도(방향벡터)로 회전시키는 함수
transform.Rotate(Vector3.up * 10 * Time.deltaTime);
}
//물리효과 변경을 스크립트로 수정
private void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.CompareTag("Floor"))
{
//바닥에 충돌시 키네마틱(물리충돌연산x)로 변경
rigid.isKinematic = true;
sphereCollider.enabled = false;
}
}
}
수정된 물리연산들 & 유니티 기능 정리
1.자동회전방지
이 버그의 원인은 Player <> 총알탄피 의 물리충돌로 인하여 Player의 Rigidbody에 회전속력이 추가되기에 발생되는 것이다.
private void Move()
{
//무기 교체시 or 무기 공격시 or 재장전시 움직이지 못하게 설정
if (isSwap || !isFireReady || isReload) moveVec = Vector3.zero;
//위의 조건과 같이 걸어버리면 회전할시에 moveVec에 영향을 받게 되서 캐릭터가 굳어버릴수 있다.
if (!isBorder)
{
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);
}
Move()함수에 추가하여 transform에 영향을 안주게 막아버렸다. 이러면 애니메이션은 계속 재생하기에
플레이어가 키를 눌렀다는것을 인지함과 동시에 벽 관통 버그를 원천 봉쇄해버렸다.
4.아이템 물리 총돌 제거
물리 충돌을 담당하는 콜라이더와 Player가 충돌시 문제가 발생한다.
Item.cs
private void Awake()
{
rigid = GetComponent<Rigidbody>();
//Item 오브젝트에는 2개의 스피어콜라이더가 있음
//GetComponent 호출시 Inspector 제일 위에있는 컴포넌트를 호출하게됨
sphereCollider = GetComponent<SphereCollider>();
}
//물리효과 변경을 스크립트로 수정
private void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.CompareTag("Floor"))
{
//바닥에 충돌시 키네마틱(물리충돌연산x)로 변경
rigid.isKinematic = true;
sphereCollider.enabled = false;
}
}
Floor와 충돌할때, Rigid를 키네마틱 ,물리충돌 담당하는 콜라이더를 비활성화 시켜 Player와의 충돌을 방지하였다.
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 몸에 장착되는 오브젝트)의 콜라이더 관리
1-1.빨간상자 의 Collider는 땅위에 서있어야 되기때문에 추가
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.무기 오브젝트를 획득할때 스크립트로 무기를 넣어 오브젝트를 생성해줘야함?
물론, 그렇게 해도 된다. 하지만, 이번 프로젝트에서는 다른 방법으로 처리를 하였다.
우선 무기오브젝트를 장착할 때, 어떡해 하는지를 메모하겠다.
보통 3D게임에서 무기를 관리할떄 이 방법을 많이 사용하는것 같다.
우선 오른손의 무기 잡는 위치를 조정해줘야한다.
손 안으로 실린더를 집어넣어 위치를 잡아주고 실린더는 빈오브젝트 처럼 보여야 되기 땜누에 Renderer 및 Collider를 비활성화 제거를 해준다.
해당 오브젝트 자식으로 무기 오브젝트들을 전부 자식으로 넣은뒤 비 활성화 해주고, 이 오브젝트들을 최상위 부모 클래스인 Player의 Inspector를 통해 Weapons 배열에 삽입해준다.
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는 오브젝트의 잔상을 그려주는 기능이다.
5.Particle System 기능 설명
여기서 설명하는 Particle System 기능은 이동시에 파티클을 생성하는 기능을 설정하는 방법이다.
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;
해당 코드에서 대각선으로 이동시 캐릭터가 더 빠르게 움직이게 된다.
그렇기 때문에 해당 벡터의 크기를 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에 적용
4.Animater의 Parameter 결정 조건
Roop Animation들은 보통 bool 파라미터를 사용한다.
일회성 애니메이션(점프,회피)은 Trigger 파라미터를 사용한다.
Trigger 파라미터는 doXXX로 작성 하였고 스크립트에서 On을 하면 애니메이션이 재생되고
애니메이션이 끝난 후 Off하게 되는 파라미터이다.
5.3D에 오브젝트(부모,자식)배치하기 - 하이어라키(계층 구조의 이해)
부모오브젝트는 (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
Transforn . y 값 조절 : 위치를 조절하여 빛의 위치 조정
Light - Intensity : 빛의 세기 조절
Light - Range : 빛의 범위 조절
마지막 움짤을 보면 아이템 밑에 부위에도 빛이 비치는 장면이 있다.
*Particle System 간단 설명
Material을 변경하여 파티클 이미지를 변경 할 수 있다.
Emission(방출)은 파티클의 갯수에 영향을 준다. Time의 숫자가 커질수록 파티클의 갯수가 증가한다.
Shape 기능은 파티클이 어떤 방식으로 뻗어 나갈건지 결정하는 기능이다. Corn 모양일때 원뿔 모양에서 파티클이 뿜어져 나왔고 Donut일때는 사방으로 퍼져 나오는 모습을 볼 수있으며 퍼져나오는 근원의 위치,각도,크기를 조절 할 수 있다.
Color over LifeTime 은 파티클의 색상을 결정하는 기능이다. 해당 이미지를 보면 이해가 될것이다.
Size over LifeTime 은 파티클이 살아있는동안 크기를 변화하는 기능이다.
Unity에서 제공하는 프리셋이 있어 그걸 사용해도 된다. 위의 이미지 처럼 위로 올라갈수록 파티클의 크기가 커지는 모습을 볼 수있다.