일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- COSMO
- 기초
- 에이세프라이트
- 자원순환보증금관리센터
- pixel art
- 서포터즈
- 장학팀
- 멋쟁이사자처럼
- 노하우
- 도트
- 드로잉 연습
- Pixelart
- 애니메이션
- 픽셀 아트
- 드로잉
- 포토샵
- 스마일게이트
- layer
- photoshop
- menu
- 개발
- Aseprite
- 모작
- 픽셀아트
- 연습
- 도트공부
- 인디게임 개발
- TOOL
- 반환원정대
- 채색
- Today
- Total
소소한 나의 하루들
2d 플랫포머(8) - 스테이지를 넘나드는 게임 완성하기 본문
출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared
#1. 플레이어 이동 (로직 수정)
오른쪽 키를 누른 상태에서 왼쪽 키를 누르면, flip이 적용되지 않아서 문워크를 하게되는 모습이 보이는데 GetButtonDown은 키 입력이 겹치는 구간에서 문제가 발생한다. (GetButtonDown이 적용이 안되었기 때문)
따라서 GetButtonDown을 GetButton으로 변경한다.
#2. 몬스터 잡기
마리오처럼 몬스터를 밟아서 점수를 얻어보도록 한다.
당연히 충돌을 담당하는 OnCollisionEnter2D에서 작성한다. 앞에서 OnDamaged라는 함수를 작성했었다.
여기서 분기점을 나눠본다.
gameObject인 Player가 충돌한 것이 Enemy 레이어에 있는 오브젝트일 때 Player가 몬스터보다 위에 있고, 아래로 낙하 중이라면 = 밟았다는 판정 → 공격
if(rigid.velocity.y < 0 && transform.position.y > collision.transform.position.y)
현재속도(velocity) y축이 0보다 작으면서(아래로 내려가면서) 현재위치 y성분이 피격위치 y성분보다 크다면
OnAttack( ) 함수에 몬스터의 죽음관련 함수를 호출한다.
PlayerMove 스크립트에서 EnemyMove 스크립트의 OnDamaged 함수를 호출한다. (외부 스크립트의 함수를 호출한다)
public void OnMonsterDamaged()
{
//Sprite Alpha
sprite.color = new Color(1, 1, 1, 0.4f);
//Sprite Flip Y
sprite.flipY = true;
//Collider Disable
collider.enabled = false;
//Die Effect Jump
rigid.AddForce(Vector2.up * 5, ForceMode2D.Impulse);
//Destroy
Invoke("Deavtive", 5);
}
void Deavtive()
{
gameObject.SetActive(false);
}
EnemyMove 스크립트의 몬스터 죽음 함수인 OnDamaged에는 몬스터가 죽었을 때 취해야 하는 액션을 구현한다.
몬스터가 공격을 받았을 때
1. 투명도가 낮아지고 (sprite Renderer 컴포넌트의 color 값 활용)
2. 애니메이션이 상하 반전이 되고 (sprite Renderer 컴포넌트의 flipY 옵션 활성화)
3. 무적상태가 되고 (Capsule Collider 2D 컴포넌트를 .enable 메소드로 비활성화) *컴포넌트를 비활성화 컴포넌트변수.enable
4. 죽음 이펙트 점프 효과 (위로 튀어올랐다가 추락) (rigid.AddForce() 메소드로 튀어오르도록 설정)
5. 파괴됨 (오브젝트 비활성화) (gameObject를 .SetActive()로 비활성화 시키는 함수를 5초뒤에 실행) *오브젝트를 비활성화 gameObject.SetActive(값)
외부에서 불러와야하는 함수이기 때문에, public으로 설정한다.
무적상태같은 경우 Player가 피격 시 3초간 무적상태로 다른 레이어로 변환되는 것이 아니라, Collider 컨포넌트를 비활성화 해야한다.
따라서 현재 Enemy 오브젝트에 적용되어있는 Collider 컴포넌트를 스크립트로 불러와서 비활성화(false)해줘야한다. 비활성화 로직은 시간차를 두어 실행한다.
그리고 Player가 적을 밟았을 때, 플레이어에게도 반발력을 주면 좋다. (플레이어가 점프를 뛰도록)
※변수명을 그냥 collider를 사용하면, 경고를 준다. (이미 정의되어있는 이름)
#3. 아이템
아이템은 Player와 Enemy와 마찬가지로, Collider + Sprite + Animation 작업으로 아이템을 구현한다.
*물리 충돌을 하면 안되니까 isTrigger를 체크한다. (물리충돌없이 신호만 받음)
그리고 Tag는 Item으로 설정한다.
동전을 먹었을 때 동전이 사라지도록 구현해본다.
OnCollisoinEnter2D가 아니라, 동전은 OnTriggerEnter2D이다. (IsTrigger에 체크했으니까)
아이템들은 태그도 Item으로 설정한다.
#4. 결승점
finish 깃발은 Box Collider 2D 컴포넌트를 추가하고, 크기에 맞게 적절히 offset과 size를 설정한다.
결승점(깃발) 또한 태그를 Finish로 설정한다 (Finish는 기본으로 있음)
*물리 충돌을 하면 안되니까 isTrigger를 체크한다. (물리충돌없이 신호만 받음)
Collider 컴포넌트의 istrigger는 물리 충돌을 하지 않는 대신, 충돌했다는 신호만 발생하기위한 오브젝트에 체크 →OnTriggerEnter2D에 작성
Next Stage를 플레이어가 하나? 아니다. 이것은 manager(매니저)가 해야한다.
#5. 매니저
Hierarchy에서 우클릭>Create Empty 클릭해서 GameObject만든다.
그리고 Game Manager라는 스크립트 파일을 하나 만든다.
스크립트 파일을 GameManager 오브젝트에 컴포넌트로 적용한다.
매니저는 점수와 스테이지를 관리한다. 따라서 점수와 스테이지 전역 변수를 생성한다.
플레이어가 몬스터를 밟았을 때 stagePoint를 올린다.
따라서 플레이어에서 게임 매니저 변수를 만들어서 점수 변수에 접근할 수 있도록 한다.
Player 오브젝트의 Inspector에서 PlayerMove 스크립트 컴포넌트의 Game Manager 변수 란에다가 GameManager 오브젝트를 드래그하여 적용시킨다.
그러면 이제 GameManager 안에 있는 것들을 사용 가능하다.
contains 함수는 검색 인수에서 지정하는 기준을 사용하여 텍스트 검색 인덱스를 검색하고 일치를 발견했는지 여부를 표시하는 값을 리턴한다. true/false
bool isBronze = collision.gameObject.name.Contains("Bronze");
부딪힌 오브젝트의 이름에 “Bronze”가 포함되어있는지 판단하여 isBronze에 대입 (논리값)
아이템 등급 (금/은/동)에 따라 점수를 다르게 얻을 수 있게 코드를 짠다.
이제 Finish 깃발으로 들어가게되면, 아이템을 얻고/몬스터를 잡아서 얻은 점수가 전체 점수로 적용되게끔 구현한다.
그러면서 스테이지까지 같이 옮겨져야한다. 스테이지가 올라가면 totalPoint에 누적된다.
NextStage라는 함수는 PlayerMove의 OnTriggerEnter2D의 //Next Stage에 작성해주면 된다.
몬스터에게 피격당했을 때의 피해, 낭떠러지에 떨어졌을 때의 피해를 Hp라는 개념을 만들어서 GameManager에서 관리해본다. 매니저에 health 변수를 추가한다.
낭떠러지는 Game Manager오브젝트에서 만들어본다. (Box Collider 2D로)
Offset: 현재위치를 기준으로 충돌 경계선의 위치를 x,y축을 기준으로 변경합니다.
Is Trigger: 해당 물체가 다른 물체와 충돌 여부를 선택하는 속성입니다. (True:충돌을 안한다. False:충돌을 안한다.)
Box Collider 2D 컴포넌트를 GameMager오브젝트에 추가하고, Offset을 -3으로 Size는 x를 쭉 100으로 늘려준다.
그리고 충돌하지 않도록, Is Trigger를 체크해준다.
낭떠러지에 떨어졌을 때 체력이 닳음.
Player가 피격을 받아 데미지를 잃었을 때에도 체력이 닳음.
그리고 낭떠러지 로직에 플레이어 원위치 기능도 추가한다.
attachedRigidbody를 입력하면 바로 RigidBody를 가져온다. 낙하속도는 0으로 만든다.
※이 로직은 플레이어의 시작지점이 반드시 (0, 0)이어야 가능하다.
현재 지형 오브젝트의 위치를 고려해서 새로 원위치 될 좌표를 잘 설정해줘야 한다.
health가 0이라는 것은 죽었다는 것이므로, 죽었을 때 이펙트까지 기능을 구현해준다. → 플레이어의 체력이 0이 되면 플레이어의 죽음 함수를 호출한다.
EnemyMove에서 OnDamaged 함수의 로직 일부를 가져온다.
GameManager의 Player란에 Player 오브젝트를 드래그하여 적용시킨다.
마찬가지로 PlayerMove에서도 gameManager의 HealthDown 함수를 호출해서 피격 시 체력이 닳거나, 체력이 0이라면 OnDie함수를 호출할 수 있도록 한다.
Game Manager의 낭떠러지 추락 시에도 HealthDown 함수를 호출한다.
그리고 낭떠러지 추락 시 원위치는, 체력이 0이되면 원위치시키지 않도록 한다.
#6. 스테이지
Player, GameManager를 제외한 나머지는 전부 Stage1 오브젝트에 넣는다.
(Stage1에 대한 게임 오브젝트를 하나로 집어넣어 정리)
그리고 이런 스테이지를 여러개 만든다. (작업하려는 스테이지만 활성화한다.)
각 스테이지를 서로 다른 모습의 맵으로 구상해도 된다. 스테이지 3개를 만들었으면, Game Manager에서 스테이지들을 관리한다.
GameObject를 배열로 Stages라는 변수를 선언하고, stageIndex를 사용해서 Stages를 연다. (오브젝트를 배열로 관리)
: stageIndex에 따라 스테이지 활성화/비활성화
다음 스테이지로 이동하면, 다시 원점(출발지점)으로 이동해야한다.
이미 전에 유사한 로직을 작성한 적이 있는데, 여기서의 Rigidbody는 collision한 오브젝트에 있는 것이라서 따로 함수를 작성해준다. (원점으로 이동하는 함수)
속도를 0으로 만드는 VelocityZero 함수를 만든다.
이렇게 기능적으로 다 세분화시키는 것이 좋다.
낭떠러지에서 떨어졌을 때 원위치하는 로직도 PlayerReposition 함수로 대체해줄 수 있다.
스테이지 이동 시 플레이어 원점 이동도 PlayerReposition 함수로 구현해준다.
이제 스테이지 개수를 확인해서 다음 스테이지로 이동할지 / 종료할지도 구현한다.
그리고 마지막 스테이지를 클리어해서 완주하게 되면, timeScale = 0으로 시간을 멈춰둔다.
이제 저장하고 실행시키면 Stages 배열의 원소들을 추가할 수 있도록 컴포넌트에 옵션이 나온다.
그리고 메인 카메라는 플레이어 오브젝트 아래로 이동시킨다. (자식 오브젝트로 이동) : 이제 카메라가 플레이어를 비춰준다.
#7. UI
우선 Hierarchy에서 우클릭>UI>Canvas를 클릭해서 Canvas UI를 추가한다. 앵커를 활용하면서 체력, 스테이지, 점수 UI를 배치한다. 마지막으로 재시작 버튼 UI를 생성한다.
이제 Game Manager에다가 등록해서 관리한다.
※스크립트에 UI 관련 요소를 작성하려면 UnityEngine.UI를 using해야한다.
Image UI는 배열로 작성한다.
이렇게 작성한 후 저장하면 유니티에서 Game Manager의 컴포넌트에 나타난다.
버튼 관련 요소도 작성한다.
점수는 Update에다 작성해준다.
UIPoint.text = totalPoint + stagePoint;
여기서 totalPoint와 stagePoint는 int형 숫자이다. 따라서 묶어서 ToString()으로 String형 변환 해주면 된다.
그리고 Stage UI도 스테이지에 따라 다르게 표시될 수 있도록 한다.
체력은 health 값으로 해당 이미지 색상을 어둡게 변경한다. 재시작버튼은 게임이 끝났을 때와 죽었을 때 활성화한다.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class GameManager : MonoBehaviour
{
public int totalPoint;
public int stagePoint;
public int stageIndex;
public int health;
public PlayerMove player;
public GameObject[] Stages;
public Image[] UIhealth;
public TextMeshProUGUI UIPoint;
public TextMeshProUGUI UIStage;
public GameObject UIRestart;
private void Update()
{
UIPoint.text = "Score: " + (totalPoint + stagePoint).ToString();
}
public void NextStage()
{
//Change Stage
if (stageIndex < Stages.Length - 1)
{
Stages[stageIndex].SetActive(false);
stageIndex++;
Stages[stageIndex].SetActive(true);
PlayerReposition();
UIStage.text = "Stage " + (stageIndex + 1).ToString();
}
//Game Clear
else
{
//Player Control Lock
Time.timeScale = 0;
UIRestart.SetActive(true);
TextMeshProUGUI UIReStartText = UIRestart.GetComponentInChildren<TextMeshProUGUI>();
UIReStartText.enabled = true;
}
totalPoint += stagePoint;
stagePoint = 0;
}
그리고 버튼 UI의 텍스트(Text)는 자식 오브젝트이므로 'InChildren'을 더 붙여야 한다.
이제 버튼에 OnClick 이벤트 함수를 만들어서 연결하면 끝이다.
저장하고, Button UI Inspector에서 On Click() 이벤트 함수에 Game Manager 오브젝트 드래그 앤 드롭한다.
그리고 Game Manager 스크립트 내의 Restart 함수를 설정한다.
재시작하게되면 timeScale = 1로 시간을 복구한다.
Scene 전환
코드 위에 using UnityEngine.SceneManagement을 작성해줘야 유니티에서 제공하는 Scene 전환을 사용할 수 있다.
그리고 Scene을 전환하기 위해 SceneManager.LoadScene("이동할 Scene 이름")을 작성해주면 된다.
#8. 사운드
어떤 사운드가 필요할지 미리 정리해보면, 점프, 아이템 먹는 것, 죽음, 공격, 피격 사운드가 필요할 것 같다.
public class PlayerMove : MonoBehaviour
{
public AudioClip audioJump;
public AudioClip audioAttack;
public AudioClip audioDamaged;
public AudioClip audioItem;
public AudioClip audioDie;
public AudioClip audioFinish;
public float maxSpeed;
public float jumpPower;
public GameManager gameManager;
//private bool isJumping = false;
CapsuleCollider2D capsuleCollider;
Rigidbody2D rigid;
SpriteRenderer spriteRenderer;
AudioSource audioSource;
public Animator anime;
public Vector2 respawnPosition;
private void Awake()
{
rigid = GetComponent<Rigidbody2D>();
spriteRenderer = GetComponent<SpriteRenderer>();
anime = GetComponent<Animator>();
capsuleCollider = GetComponent<CapsuleCollider2D>();
audioSource = GetComponent<AudioSource>();
}
...
}
public void PlaySound(string action)
{
switch (action)
{
case "Jump":
audioSource.clip = audioJump;
break;
case "Attack":
audioSource.clip = audioAttack;
break;
case "Damaged":
audioSource.clip = audioDamaged;
break;
case "Item":
audioSource.clip = audioItem;
break;
case "Die":
audioSource.clip = audioDie;
break;
case "Finish":
audioSource.clip = audioFinish;
break;
}
audioSource.Play();
}
플레이어에 AudioSource 컴포넌트를 추가하고, Play On Awake는 체크해제한다. 효과음으로 public으로 선언한 AudioClip 변수를 추가한다.
각 액션마다 클립을 바꾸고 재생하는 PlaySound() 함수를 생성한다. 이때 action에 대해 switch-case문을 활용하면 효율적이다. *마지막에 default문을 작성해줬어야했다.
그리고 각 액션을 구현하는 로직에 PlaySound()을 적절한 인수를 넣어서 실행할 수 있도록 해준다.
audioSource.clip = audioJump;
audioSource.Play();
따로 PlaySound() 함수를 작성하지 않고, 이렇게 각 액션 구현 로직마다 이렇게 작성해줄 수 도 있을 것이다. 하지만 사운드나 어떤 동작 하나에 대해서 한꺼번에 관리해줄 수 있는 함수를 작성하는 것이 코드 관리 측면에서 효율적이다.
마지막에 AudioClip 변수에 알맞은 효과음 파일을 넣으면 완성이다.
실행시켰을 때 소리가 제대로 나지 않는다면, Game창에서 음소거를 활성화하지는 않았는지 꼭 체크해준다. (이것때문에 꽤 고생했다..)
'개발 > 유니티' 카테고리의 다른 글
TextMeshPro 활성화 /비활성화 (ft. SetActive() vs .enabled) (0) | 2024.01.26 |
---|---|
유니티 TextMeshPro(TMP) UI 래퍼런스 할당 (0) | 2024.01.26 |
2d 플랫포머(7) - 플레이어 피격 이벤트 구현하기 (0) | 2024.01.24 |
2d 플랫포머(6) - 몬스터 AI 구현하기 (0) | 2024.01.24 |
2d 플랫포머(5) - 타일맵으로 플랫폼 만들기 (0) | 2024.01.23 |