이번 포스팅에서는 #21 유니티 오브젝트 풀링(Object Pooling) 포스팅에 이어서 책 : 유니티 2D 게임 개발(게임 개발 프로그래밍)유니티와 c#으로 시작하는 인디게임 개발의 마지막이야기를 진행하겠습니다.
포스팅은 유니티 2D 게임 개발(게임 개발 프로그래밍)에 나온 예제로 진행합니다.
저번 포스팅에이어서 무기발사하는 모션의 애니메이션과 적이나 플레이어가 공격을 당했을 때 깜박이는 효과, 마지막 빌드 까지 진행하겠습니다.
무기발사에 사용할 플레이어 애니메이션에 사용할 스프라이트를 임포트하였습니다.
Sprite Mode : Multiple
Pixels Per Unit : 32
Filter Mode : Point(no filter)
Compression : None
Apply눌러 적용하고 Sprite Editor를 눌러 에디터를 엽니다.
1. Slice > Type > Gird By Cell Size 를 선택
2. Pixel Size : 32, 32 입력
Slice 눌러 자르고 Apply 선택합니다.
각 스프라이트를 선택하고 애니메이션 4개를 만들었습니다. 각 애니메이션은 무기발사시에 방향을 담당합니다.
PlayerController 애니메이터를 열고 새로운 블렌드 트리를 만들겠습니다.
그 이후의 과정은 아래의 링크를 참고 하시면 되겠습니다. 블렌드트리의 노드를 만드는 과정은 생략하겠습니다.
Animator의 Parameters와 Blend Tree의 Parameters와 motion 속성 설정입니다. 이 또한 링크를 참고하시면 되겠습니다.
Animator의 Base Layer로 넘어와서 player-idle과 Fire Tree와 전환을 생성해줍니다.
player-idle > Fire Tree로 이어지는 전환을 선택하고 사진과 같이 설정합니다.
Fire Tree > player-idle로 이어지는 전환의 속성은 Has Exit Time 속성을 체크하고 Exit Time 속성에 1을 입력하였고Conditions > isFiring 속성을 false로 설정하였습니다.
Has Exit Time 속성
전환의 종료 시간 속성은 애니메이션을 몇 퍼센트까지 재생한 뒤에 전환할지 애니메이터에게 알려주는 역할을 합니다.
대기로 전환하는 전환의 종료 시간 속성을 1로 설정하면 전환하기 전에 발사 애니메이션을 100% 재생하고 싶다는 의미입니다.
Weapon 스크립트(C#)을 수정하겠습니다.
Weapon 스크립트(C#)의 전체코드입니다.
using System;
using System.Collections.Generic;
using UnityEngine;
//수정한 부분 <-------
[RequireComponent(typeof(Animator))]
// --------->
public class Weapon : MonoBehaviour
{
//생략한 부분
//수정한 부분 <--------
bool isFiring;
[HideInInspector]
public Animator animator;
Camera localCamera;
float positiveSlope;
float negativeSlope;
enum Quadrant
{
Left,
Right,
UP,
Down
}
// --------->
//수정한 부분 <--------
void Start()
{
animator = GetComponent<Animator>();
isFiring = false;
localCamera = Camera.main;
Vector2 lowerLeft = localCamera.ScreenToWorldPoint(new Vector2(0, 0));
Vector2 lowerRight = localCamera.ScreenToWorldPoint(new Vector2(Screen.width, 0));
Vector2 upperLeft = localCamera.ScreenToWorldPoint(new Vector2(0, Screen.height));
Vector2 upperRight = localCamera.ScreenToWorldPoint(new Vector2(Screen.width, Screen.height));
positiveSlope = GetSlope(lowerLeft, upperRight);
negativeSlope = GetSlope(upperLeft, lowerRight);
}
// ---------->
void Awake()
{
if (ammoPool == null)
{
ammoPool = new List<GameObject>();
}
for (int i = 0; i < poolSize; i++)
{
GameObject ammoObject = Instantiate(ammoPrefab);
ammoObject.SetActive(false);
ammoPool.Add(ammoObject);
}
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
//수정한 부분 <------
isFiring = true;
// -------->
FireAmmo();
}
//수정한 부분 <------
UpdateState();
// -------->
}
//수정한 부분 <------
private void UpdateState()
{
if (isFiring)
{
Vector2 quadrantVector;
Quadrant quadEnum = GetQuadrant();
switch (quadEnum)
{
case Quadrant.Left:
quadrantVector = new Vector2(-1.0f, 0.0f);
break;
case Quadrant.Right:
quadrantVector = new Vector2(1.0f, 0.0f);
break;
case Quadrant.UP:
quadrantVector = new Vector2(0.0f, 1.0f);
break;
case Quadrant.Down:
quadrantVector = new Vector2(0.0f, -1.0f);
break;
default:
quadrantVector = new Vector2(0.0f, 0.0f);
break;
}
animator.SetBool("isFiring", true);
animator.SetFloat("fireXDir", quadrantVector.x);
animator.SetFloat("fireYDir", quadrantVector.y);
isFiring = false;
}
else
{
animator.SetBool("isFiring", false);
}
}
float GetSlope(Vector2 pointOne, Vector2 pointTwo)
{
return (pointTwo.y - pointOne.y) / (pointTwo.x - pointOne.x);
}
bool HigherThanPositiveSlopeLine(Vector2 inputPosition)
{
Vector2 playerPosition = gameObject.transform.position;
Vector2 mousePosition = localCamera.ScreenToWorldPoint(inputPosition);
float yIntercept = playerPosition.y - (positiveSlope * playerPosition.x);
float inputIntercept = mousePosition.y - (positiveSlope * mousePosition.x);
return inputIntercept > yIntercept;
}
bool HigherThanNegativeSlopeLine(Vector2 inputPosition)
{
Vector2 playerPosition = gameObject.transform.position;
Vector2 mousePosition = localCamera.ScreenToWorldPoint(inputPosition);
float yIntercept = playerPosition.y - (negativeSlope * playerPosition.x);
float inputIntercept = mousePosition.y - (negativeSlope * mousePosition.x);
return inputIntercept > yIntercept;
}
Quadrant GetQuadrant()
{
bool higherThanPositiveSlopeLine = HigherThanPositiveSlopeLine(Input.mousePosition);
bool higherThanNegativeSlopeLine = HigherThanNegativeSlopeLine(Input.mousePosition);
if (!higherThanPositiveSlopeLine && higherThanNegativeSlopeLine)
{
return Quadrant.Right;
}
else if (!higherThanPositiveSlopeLine && !higherThanNegativeSlopeLine)
{
return Quadrant.Down;
}
else if (higherThanPositiveSlopeLine && !higherThanNegativeSlopeLine)
{
return Quadrant.Left;
}
else
{
return Quadrant.UP;
}
}
// -------->
//생략한부분
}
코드가 너무 길어 생략한 부분이 있는점 양해부탁드립니다.
수정한 부분만 확인해보겠습니다.
[RequireComponent(typeof(Animator))]
RequireComponent 코드로 항상 해당 컴포넌트를 사용할 수 있게 해줍니다. 우리는 Animator 컴포넌트를 사용할 수 있게 해줍니다.
bool isFiring;
[HideInInspector]
public Animator animator;
Camera localCamera;
float positiveSlope;
float negativeSlope;
enum Quadrant
{
Left,
Right,
UP,
Down
}
bool형식의 isfiring은 플레이어가 무기를 발사중인지 나타내는 변수입니다.
[HideInInspector] 와 public 접근자 키워드를 함께 사용한 animator 변수는 클래스 밖에서 접근 할 수 있지만 인스펙터 창에는 나타나지 않습니다.
코드를 통하여 애니메이터 컴포넌트의 참조를 가져올 것입니다.
localCamera에 카메라 참조를 저장해 놓을 것입니다.
positiveSlope, negativeSlope 변수는 기울기를 저장할 것입니다.
Quadrant 열겨형식은 플레이어가 무기를 발사할 때 방향을 지정할 것입니다.
GetSlope() 메서드부터 확인하겠습니다.
float GetSlope(Vector2 pointOne, Vector2 pointTwo)
{
return (pointTwo.y - pointOne.y) / (pointTwo.x - pointOne.x);
}
GetSlope() 메서드를 보기 전에 우리가 알아야 할 것이 있습니다.
플레이어를 기준으로 사용자가 클릭한 방향을 알아내야 재생할 애니메이션을 결정할 수 있습니다.
사용자가 클릭한 방향을 알아내는 방법으로 우리는 네 개의 사분면을 가지고 알아 보겠습니다.
일차 함수의 공식을 사용하면
y = mx + b
이때
m = 기울기
x, y는 점의 좌표
b = y 절편 즉 직선고 y축의 교차점입니다.
마우스 클릭을 기준으로 기울기가양인 직선이 플레이어를 기준으로 기울기가 양인 직선보다 위에 있는지 아래에 있는지를 바탕으로 사용자가 클릭한 사분면을 파악할 수 있습니다.
기울기가 같은 두 직선은 서로 평행하고 두 직선의 y절편을 비교하면 위나 아래에 있느 직선을 알 수 있습니다.
그림처럼 마우스 클릭 위치에서 기울기가 음인 직선의 y절편은 플레이어의 위치에서 기울기가 음인 직선의 y절편보다 작습니다.
또한 마우스 클릭 위치에서 기울기가 양인 직선의 y절편은 플레이어의 위치에서 기울기가 양인 직선의 y 절편보다 큽니다.
이렇듯 우리는 y절편을 비교하겠습니다. y절편을 쉽게 비교하기 위해 공식을 조금 재구성하였습니다.
공식 : b = y - mx
y절편 B를 구하기전에 m(기울기)를 먼저 구하겠습니다.
직선상에 있는 두 점 사이의 기울기를 구하는 공식 (y2 - y1) / (x2 - x1) 입니다.
GetSlope() 메서드를 확인해보면 Vector2 형식으로 pointOne과 PointTwo를 매개변수로 사용합니다.
이때 Vector2 형식은 (x, y)를 가지고 있습니다.
즉 pointOne = (x1, y1) , pointTwo = (x2, y2) 를 가지고 있습니다.
공식에 대입해 보면
(pointTwo.y - pointOne.y) / (pointTwo.x - pointOne.x) 입니다.
이것이 바로 GetSlope() 메서드의 return 값입니다.
기울기를 구한 GetSlope()메서드를 사용한 Start() 메서드를 확인하겠습니다.
Start() 메서드 입니다.
void Start()
{
animator = GetComponent<Animator>();
isFiring = false;
localCamera = Camera.main;
Vector2 lowerLeft = localCamera.ScreenToWorldPoint(new Vector2(0, 0));
Vector2 lowerRight = localCamera.ScreenToWorldPoint(new Vector2(Screen.width, 0));
Vector2 upperLeft = localCamera.ScreenToWorldPoint(new Vector2(0, Screen.height));
Vector2 upperRight = localCamera.ScreenToWorldPoint(new Vector2(Screen.width, Screen.height));
positiveSlope = GetSlope(lowerLeft, upperRight);
negativeSlope = GetSlope(upperLeft, lowerRight);
}
Vector2 형식으로 loweLeft, lowerRight, upperLeft, upperRight 네 구석을 나타내는 네 개의 벡터를 만들었습니다.
그림을 보면 알 수 있듯이 유니티의 화면 좌표계는 화면의 왼쪽 아래(0, 0)에서 시작합니다.
대입하기 전에 각 위치를 화면 좌표계에서 월드 좌표계로 변환합니다.
positiveSlope는 양의 기울기를 담당합니다. GetSlope() 메서드를 사용하여 (0, 0)에서 (Screen.width, Screen.height)의 기울기를 구합니다. 왼쪽 아래에서 오른쪽 위로 올라가는 직선입니다.
negativieSlope는 음의 기울기를 담당하고 왼쪽 위에서 오른쪽 아래로 내려가는 직선입니다.
기울기를 구하였으니 y 절편을 비교하는 HigherThanHigherThanPositiveSlopeLine() 메서드를 확인하겠습니다. 이 메서드는 마우스가 클릭한 위치가 플레이어를 기준으로 기울기가 양인 직선보다 위에있는지 비교하는 메서드입니다.
HigherThanHigherThanPositiveSlopeLine() 메서드 입니다.
bool HigherThanPositiveSlopeLine(Vector2 inputPosition)
{
Vector2 playerPosition = gameObject.transform.position;
Vector2 mousePosition = localCamera.ScreenToWorldPoint(inputPosition);
float yIntercept = playerPosition.y - (positiveSlope * playerPosition.x);
float inputIntercept = mousePosition.y - (positiveSlope * mousePosition.x);
return inputIntercept > yIntercept;
}
playerPosition에 플레이어의 위치를 대입합니다.
mousePositon 역시 사용자가 마우스로 클릭한 위치를 대입합니다.
위에 이야기 하였던 공식을 다시 가져와 보겠습니다.
공식 : b = y - mx
공식에 대입해 보겠습니다.
b(yIntercept) = y(playerPositon.y) - m(positiveSlope) * x(playerPositon.x);
플레이어 위치의 y절편과 마우스 클릭 위치의 y절편을 구합니다.
마우스 클릭 위치의 y 절편이 플레이어 위치의 y절편 보다 크다면 true를 반환합니다.
HigherThanNegativeSlopeLine() 메서드 또한 같은 구조입니다. 생략하도록 하겠습니다.
HigherThanHigherThanPositiveSlopeLine() 메서드와 HigherThanNegativeSlopeLine() 메서드를 사용하는 GetQuadrant() 메서드를 확인하겠습니다.
GetQuadrant() 메서드 입니다.
Quadrant GetQuadrant()
{
bool higherThanPositiveSlopeLine = HigherThanPositiveSlopeLine(Input.mousePosition);
bool higherThanNegativeSlopeLine = HigherThanNegativeSlopeLine(Input.mousePosition);
if (!higherThanPositiveSlopeLine && higherThanNegativeSlopeLine)
{
return Quadrant.Right;
}
else if (!higherThanPositiveSlopeLine && !higherThanNegativeSlopeLine)
{
return Quadrant.Down;
}
else if (higherThanPositiveSlopeLine && !higherThanNegativeSlopeLine)
{
return Quadrant.Left;
}
else
{
return Quadrant.UP;
}
}
GetQuadrant()메서드는 사용자가 클릭한 사분면을 Quadrant 열거형으로 반환합니다.
사용자의 클릭 위치를 기울기가 양, 음인 직선보다 위에있는지 확인합니다.
if - else 문으로 네 개의 사분면을 확인하고 일치하는 Quadrant 값을 반환합니다.
UpdateState() 메서드는 애니메이션을 보여주는 역할을 합니다.
UpdateState() 메서드 입니다.
private void UpdateState()
{
if (isFiring)
{
Vector2 quadrantVector;
Quadrant quadEnum = GetQuadrant();
switch (quadEnum)
{
case Quadrant.Left:
quadrantVector = new Vector2(-1.0f, 0.0f);
break;
case Quadrant.Right:
quadrantVector = new Vector2(1.0f, 0.0f);
break;
case Quadrant.UP:
quadrantVector = new Vector2(0.0f, 1.0f);
break;
case Quadrant.Down:
quadrantVector = new Vector2(0.0f, -1.0f);
break;
default:
quadrantVector = new Vector2(0.0f, 0.0f);
break;
}
animator.SetBool("isFiring", true);
animator.SetFloat("fireXDir", quadrantVector.x);
animator.SetFloat("fireYDir", quadrantVector.y);
isFiring = false;
}
else
{
animator.SetBool("isFiring", false);
}
}
Update() 메서드를 확인해보면 사용자가 마우스 버튼을 누르면 is Firing이 true로 설정하는 코드가 있습니다.
블렌드트리로 전달할 값을 저장할 Vector2형식의 quadrantVector를 만듭니다.
quadEnum에 GetQuadrant()의 값을 저장합니다.
swtich()문을 통하여 사분면을 판별합니다.
Animatior의 isFiring 파라미터를 true로 설정하고 무기를 발사하는 블렌드 트리(Fire Tree)로 전환합니다.
Animator의 fireXDir, fireYDir 변수에 사용자가 클릭한 사분면에 해당하는 값을 설정합니다.
그리고 다시 is Firing을 false로 되돌려 놓습니다.
else문은 isFiring이 false라는 의미이므로 animator의 isFiring 파라미터도 false로 만듭니다.
마지막으로 구현해야 할 것은 플레이어와 적이 공격을 당했을 때 깜빡이는 효과를 주는 것입니다.
Character 클래스와 Player, Enemy 클래스를 수정할 것입니다.
먼저 Character 스크립트(C#)의 전체 코드입니다.
using System.Collections;
using UnityEngine;
public abstract class Character : MonoBehaviour
{
public float maxHP;
public float StartingHP;
public virtual void KillCharacter()
{
Destroy(gameObject);
}
public abstract void ResetCharacter();
public abstract IEnumerator DamageCharacter(int damage, float interval);
//수정한 부분 <-------
public virtual IEnumerator FlickerCharacter()
{
GetComponent<SpriteRenderer>().color = Color.red;
yield return new WaitForSeconds(0.1f);
GetComponent<SpriteRenderer>().color = Color.white;
}
// --------->
}
수정한 부분을 확인해보겠습니다.
스프라이트 렌더러 컴포넌트의 color 속성에 Color.red 값을 대입하고 빨갛게 만들고 0.1초 동한 실행을 양보한뒤에 흰색으로 되돌립니다.
Player, Enemy 클래스를 수정할 것입니다.
각 클래스의 DamageCharacter() 메서드 안에 while() 루프의 맨 위에 이 코드를 추가합니다.
public override IEnumerator DamageCharacter(int damage, float interval)
{
while (true)
{
//추가할 코드
StartCoroutine(FlickerCharacter());
스크립트를 작성하였으면 저장을 합니다.
게임을 실행해서 지금 까지 수정한 내용을 확인합니다.
게임을 끝낼 수 있는 방법인 ESC눌러 게임을 끝내는 기능을 추가하겠습니다.
RPGGameManager 스크립트(C#)을 수정하겠습니다.
RPGGameManger 스크립트(C#)의 전체코드입니다.
using UnityEngine;
public class RPGGameManager : MonoBehaviour
{
public RPGCameraManager cameraManager;
public static RPGGameManager sharedInstance = null;
public SpawnPoint playerSpawnPoint;
void Awake()
{
if (sharedInstance != null && sharedInstance != this)
{
Destroy(gameObject);
}
else
{
sharedInstance = this;
}
}
void Start()
{
SetupScene();
}
public void SetupScene()
{
SpawnPlayer();
}
public void SpawnPlayer()
{
if (playerSpawnPoint != null)
{
GameObject player = playerSpawnPoint.SpawnObject();
cameraManager.virtualCamera.Follow = player.transform;
}
}
//수정한 부분 <-------
private void Update()
{
if (Input.GetKey("escape"))
{
Application.Quit();
}
}
// ------->
}
수정한 부분의 코드가 esc버튼을 눌렀을때 애플리케이션이 종료하는 기능입니다.
마지막으로 Ctrl + S를 눌러 Scene을 저장합니다!
빌드를 진행하겠습니다.
File > Build Settings를 선택합니다.
Add Open Scenes를 눌러 필요한 씬을 추가할 수도 있고 다양한 플랫폼으로 빌드할 수 있습니다.
Build 버튼을 누르면 빌드가 시작합니다.
책에서 제공한 내용은 여기까지 입니다.
기회가 된다면 다양한 기능을 추가하겠습니다!
감사합니다! :)
'유니티2D' 카테고리의 다른 글
#21 유니티 오브젝트 풀링(Object Pooling) (5) | 2020.12.24 |
---|---|
#20 유니티 인공지는 적(Enemy) 만들기 (9) | 2020.12.23 |
#19 유니티 OnCollisionEnter2D , OnCollisionExit2D (3) | 2020.12.22 |
#18 유니티 코루틴(Coroutine), Character 보강 (7) | 2020.12.21 |
#17 유니티 카메라 매니저(플레이어 추적) (7) | 2020.12.19 |