일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Pixelart
- Aseprite
- menu
- pixel art
- 노하우
- 픽셀아트
- 드로잉
- 개발
- 드로잉 연습
- 에이세프라이트
- 연습
- 모작
- 포토샵
- 채색
- 멋쟁이사자처럼
- COSMO
- 애니메이션
- 인디게임 개발
- 픽셀 아트
- 반환원정대
- photoshop
- 자원순환보증금관리센터
- 기초
- 서포터즈
- 스마일게이트
- 도트공부
- 도트
- TOOL
- layer
- 장학팀
- Today
- Total
소소한 나의 하루들
2d 플랫포머(최종) - 피드백 추가 및 개선사항 본문
문제점 - 해결
1. Finish 만났을때 다음 스테이지로 이동x
실행 전에 Stage1을 제외한 Stage2, Stage3는 모두 체크해제해서 비활성화시킴
2. 죽었을 때 Game Over x / 캐릭터 원위치x
죽었을때 Game Over 처리 : 낭떠러지에서 떨어질때&피격당했을 때 HealthDown함수를 호출(실행) Invoke() 함수로 1초 뒤 (시간이 멈추고 Restart UI 버튼을 활성화시키는) Retry() 함수를 호출
죽을 때 캐릭터 원위치 시킬 필요없을 것 같음.
3. Stage2->Stage3 이동했을때 Stage 활성화x
OnTriggerEnter2D()로 "Finish" 태그 갖고있는 깃발 만났을 때 gameManager의 NextStage() 함수 호출
4. 낙하하기 이전 지점/(혹은 체크포인트) 그대로 스폰되도록 하고싶다.
코루틴 활용
낙하하기 이전 지점 = 캐릭터가 점프하기 직전 밟고있던 플랫폼 위치를 기억
public Vector2 respawnPosition;으로 PlayerMove 스크립트에서 전역변수로 선언하고, Awake()에서 respawnPosition = transform.position; 플레이어 초기 위치로 설정
private void FixedUpdate()
{
//Move By Button Control
float h = Input.GetAxisRaw("Horizontal");
rigid.AddForce(Vector2.right * h, ForceMode2D.Impulse);
if (rigid.velocity.x > maxSpeed) //right max speed
rigid.velocity = new Vector2(maxSpeed, rigid.velocity.y);
else if (rigid.velocity.x < maxSpeed * (-1)) //left max speed
rigid.velocity = new Vector2(maxSpeed * (-1), rigid.velocity.y);
Debug.DrawRay(rigid.position, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHit = Physics2D.Raycast(rigid.position, Vector3.down, 1, LayerMask.GetMask("Platform"));
//Landing Platform
if (rigid.velocity.y < 0)
{
if (rayHit.collider != null)
{
if (rayHit.distance < 0.5f)
{
anime.SetBool("IsJump", false);
respawnPosition = rayHit.point;
//isJumping = false;
}
}
}
}
항상 rayCast를 아래 방향으로 쏘고있게 if문 밖으로 빼서 선언해주고, rayHit도 마찬가지로 아래방향으로 Platform 레이어를 대상으로 쏘도록 값을 설정해준다.
그리고 FixedUpdate() 내에서 y축으로의 이동속도가 0이하일때 플랫폼을 밟고 있다면 해당 위치를 기억하도록 하고
void PlayerReposition()
{
//player.transform.position = new Vector3(-8, 1, -1);
player.transform.position = player.respawnPosition;
player.VelocityZero();
}
GameManager 스크립트에서 Player를 원위치시키는 함수에다가 고정벡터값 대신 입력해준다.
그러면 떨어지기 직전 지점에서 스폰된다.
추가 문제
캐릭터가 스폰된 직후 낭떠러지에 떨어지면 스폰되는 위치가 (초기 스폰 x값, 0)으로 스폰되는 문제
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.U2D;
public class PlayerMove : MonoBehaviour
{
public float maxSpeed;
public float jumpPower;
public GameManager gameManager;
//private bool isJumping = false;
CapsuleCollider2D capsuleCollider;
Rigidbody2D rigid;
SpriteRenderer spriteRenderer;
Animator anime;
public Vector2 respawnPosition;
private void Awake()
{
rigid = GetComponent<Rigidbody2D>();
spriteRenderer = GetComponent<SpriteRenderer>();
anime = GetComponent<Animator>();
capsuleCollider = GetComponent<CapsuleCollider2D>();
}
private void Start()
{
respawnPosition = transform.position;
transform.position = new Vector2(-8, 2);
}
private void FixedUpdate()
{
//Move By Button Control
float h = Input.GetAxisRaw("Horizontal");
rigid.AddForce(Vector2.right * h, ForceMode2D.Impulse);
if (rigid.velocity.x > maxSpeed) //right max speed
rigid.velocity = new Vector2(maxSpeed, rigid.velocity.y);
else if (rigid.velocity.x < maxSpeed * (-1)) //left max speed
rigid.velocity = new Vector2(maxSpeed * (-1), rigid.velocity.y);
Debug.DrawRay(rigid.position, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHit = Physics2D.Raycast(rigid.position, Vector3.down, 1, LayerMask.GetMask("Platform"));
//Landing Platform
if (rigid.velocity.y < 0)
{
if (rayHit.collider != null)
{
if (rayHit.distance < 0.5f)
{
anime.SetBool("IsJump", false);
respawnPosition = new Vector2(rayHit.point.x, rayHit.point.y + 0.75f);
Debug.Log(respawnPosition);
//isJumping = false;
}
}
}
}
private void Update()
{
//Jump By Button Control
if (Input.GetButton("Jump") && !anime.GetBool("IsJump"))
{
anime.SetBool("IsJump", true);
rigid.AddForce(Vector2.up * jumpPower, ForceMode2D.Impulse);
//isJumping = true;
}
//Stop Speed
if (Input.GetButtonUp("Horizontal"))
rigid.velocity = new Vector2(rigid.velocity.normalized.x * 0.5f, rigid.velocity.y);
//Direction Sprite
if (Input.GetButton("Horizontal"))
spriteRenderer.flipX = Input.GetAxisRaw("Horizontal") == -1;
//Animation Transition
if (Mathf.Abs(rigid.velocity.x) < 0.3 && Mathf.Abs(rigid.velocity.y) < 0.1 && !anime.GetBool("IsJump"))
anime.SetBool("IsWalk", false);
else
anime.SetBool("IsWalk", true);
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Enemy")
{
//Attack
if (rigid.velocity.y < 0 && transform.position.y > collision.transform.position.y)
OnAttack(collision.transform);
//Damaged
else
OnPlayerDamaged(collision.transform.position);
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.tag == "Item")
{
//Point
bool isBronze = collision.gameObject.name.Contains("Coin1");
bool isSilver = collision.gameObject.name.Contains("Coin2");
bool isGold = collision.gameObject.name.Contains("Coin3");
if(isBronze)
gameManager.stagePoint += 50;
if (isSilver)
gameManager.stagePoint += 70;
if (isGold)
gameManager.stagePoint += 90;
//Deactive Item
collision.gameObject.SetActive(false);
}
else if (collision.gameObject.tag == "Finish")
{
//Next Stage
gameManager.NextStage();
}
}
void OnAttack(Transform enemy)
{
//Point
gameManager.stagePoint += 100;
//Reaction Force
rigid.AddForce(Vector2.up * 5, ForceMode2D.Impulse);
//Enemy Die
EnemyMove enemyMove = enemy.GetComponent<EnemyMove>();
enemyMove.OnMonsterDamaged();
}
void OnPlayerDamaged(Vector2 targetPossition)
{
//Health Down
gameManager.HealthDown();
//Change Layer (Imortal Active)
gameObject.layer = 12;
//View Alpha
spriteRenderer.material.color = new Color(1, 1, 1, 0.4f);
//Reaction Force
int dirc = transform.position.x - targetPossition.x > 0 ? 1 : -1;
rigid.AddForce(new Vector2(dirc, 1) * 7, ForceMode2D.Impulse);
//Animation
anime.SetTrigger("Damaged");
Invoke("OffDamaged", 2);
}
void OffDamaged()
{
gameObject.layer = 11;
spriteRenderer.material.color = new Color(1, 1, 1, 1);
}
public void OnDie()
{
//Sprite Alpha
spriteRenderer.color = new Color(1, 1, 1, 0.4f);
//Sprite Flip Y
spriteRenderer.flipY = true;
//Collider Disable
capsuleCollider.enabled = false;
//Die Effect Jump
rigid.AddForce(Vector2.up * 5, ForceMode2D.Impulse);
}
public void VelocityZero()
{
rigid.velocity = Vector2.zero;
}
}
스폰 직후 착지 시 좌표를 Debug.Log() 해보니 좌표가 (-8, 0)으로 출력되었다.
따라서 스폰 시 y좌표값을 더 높게 설정해주었다.
player 중심에서 아래방향으로 향하는 rayCast 빔의 거리는 캐릭터가 착지했을 때 플랫폼까지의 거리가 0.5f 이하였지만,이것은 플레이어가 플랫폼 위에 서 있는지 여부를 판단하기 위한 기준이었을 뿐 y좌표값은 0 혹은 플랫폼 구성에 따라 0 이하였다.
만약 스폰 지점을 (x값, 0)으로 하면 플롯폼 중간에서 스폰되서 정상적으로 플랫폼 위에 착지하지 못했다.
따라서 항상 Debug.log()로 해당 좌표값을 출력해보고, 실제 좌표로 이동해보고 정상적으로 스폰되지 못하는 위치인지 판단해보는게 좋을 것 같다.
5. 스테이지 별 스폰장소 다르게
그러면 낭떠러지 떨어졌을 때 스폰되는 위치 vs 맨 처음 스테이지 스폰했을 때의 위치를 구분해줘야겠다.
public void NextStage()
{
//Change Stage
if (stageIndex < Stages.Length - 1)
{
Stages[stageIndex].SetActive(false);
stageIndex++;
Stages[stageIndex].SetActive(true);
PlayerRespawn();
...
}
public void PlayerRespawn()
{
if (Stages[0].active == true)
{
player.transform.position = new Vector2(-8, 2);
}
else if (Stages[1].active == true)
{
player.transform.position = new Vector2(-7.5f, 8);
}
else
player.transform.position = new Vector2(-9.5f, 2);
}
GameManager 스크립트에 처음 player의 스폰장소를 지정하는 PlayerRespawn() 함수를 만들고, NextStage() 함수에는 다음 스테이지를 활성화시킨 후, PlayerRespawn() 함수가 호출되도록 한다.
private void Start()
{
transform.position = new Vector2(-8, 2);
}
그리고 처음 스테이지 시작 후 스폰장소도 설정해줘야하므로 PlayerMove 스크립트에 Start() 함수에 transform.position의 값으로 스폰 장소를 초기화해준다.
6. 몬스터 앞쪽 벽 만났을 때 방향 전환 _ Raycast 사용
가끔씩 몬스터가 가만히 Idle 상태로 서있는 문제 : Capsule Collider 2D를 Circle Collider 2D로 바꾸고, radius 반경을 조절
private void Start()
{
//Set Next Active
NextMove = Random.Range(-1, 2);
}
void FixedUpdate()
{
//Move
rigid.velocity = new Vector2(NextMove, rigid.velocity.y);
//Platform Check
Vector2 frontVec = new Vector2(rigid.position.x + NextMove * 0.5f, rigid.position.y);
Debug.DrawRay(frontVec, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHitDown = Physics2D.Raycast(frontVec, Vector3.down, 1, LayerMask.GetMask("Platform"));
Vector2 toFrontVec = new Vector2(NextMove, 0);
Debug.DrawRay(rigid.position, toFrontVec, new Color(1, 0, 0));
RaycastHit2D rayHitFront = Physics2D.Raycast(rigid.position, toFrontVec, 1, LayerMask.GetMask("Platform"));
if (rayHitDown.collider == null) //낭떠러지를 만났을 때(앞에 Platform 오브젝트가 없을때)
{
NextMove = NextMove * -1;
spriteRenderer.flipX = NextMove == 1;
CancelInvoke();
Invoke("Think", NextThinkTime);
}
if (rayHitFront.collider != null)
{
if (rayHitFront.distance < 0.55f)
{
//Debug.Log(rayHitFront.distance);
NextMove = NextMove * -1;
spriteRenderer.flipX = NextMove == 1;
CancelInvoke();
Invoke("Think", NextThinkTime);
}
}
}
우선 몬스터의 이동방향으로 빔을 쏘기 위해 캐릭터의 방향을 랜덤으로 결정해주는 NextMove 변수를 밖으로 빼고, 몬스터의 이동방향에 따라 길이 1만큼의 빨간색 빔을 쏘도록 설정했다. 빔은 Platform 레이어의 오브젝트만을 인식하고, 만약 인식했다면 이동방향이 반대로 바뀌고, 적당히 filpX가 활성화되도록, 몬스터가 앞이 낭떠러지임을 인식하는 것과 같은 로직을 설정했다. 아마 하나의 함수로 만들어도 될 것 같다.
무엇보다 플레이어가 점프 후 낙하했을 때 Platform 바닥 위에 서있는 간격을 0.5f로 인식하도록 했는데 몬스터의 앞이 Platform 오브젝트임을 인식하는 거리는 그보다 더 길게 측정이 되었다. 따라서 Debug.Log()를 통해 거리가 얼마나 되는지 확인하고 조건문을 작성하면 더 좋을 것 같다.
7. Retry 버튼 입력 시 처음 스테이지 스폰지점으로 이동, 처음부터 재시작
유니티 씬 전환하기 기능 사용
using UnityEngine.SceneManagement;
public void Restart()
{
Time.timeScale = 1;
if (SceneManager.GetActiveScene().name == "Start")
SceneManager.LoadScene("End");
else if (SceneManager.GetActiveScene().name == "End")
SceneManager.LoadScene("Start");
}
using UnityEngine.SceneManagement; 입력헤서 해당 네임스페이스를 사용하겠다고 선언한다. 그리고 시간물 멈추고 씬을 전환하는 Restart() 함수를 public으로 선언해서 Retry UI 버튼에 연결한다.
씬 전환은 SceneManager.GetActiveScene().name을 통해 현재 활성화된 씬이 무엇인지 판단하고, 만약 A 씬이라면 B씬으로 이동하도록 SceneManager.LoadScene("씬이름")으로 씬 전환을 한다.
마지막 종료 UI 화면이 나오도록 씬을 구성해도 좋다.
씬 생성 후에는 꼭 File>Build Settings에서 새로 생성한 Scene을 드래그 적용해서 빌드해줘야 씬 전환이 된다.
위 코드는 지금 활성화된 씬이 "Start"씬이라면, "End" 씬으로 이동하고, "End"씬이라면 "Start"씬으로 이동하도록 하는 로직이다.
8. 함정만났을 때 데미지(Only 피격)
함정을 이동 중에 만났을 때와 위에서 밟았을 때 튀는 기준을 구분해야할 것 같다.
우선 콜라이더 컴포넌트가 있지 않았기 때문에 OnCollisionEnter2D() 함수가 실행되질 못했다.
그리고 플레이어가 점프하여 위에서 가시함정를 밟았을 때 Null Reference 에러가 발생했다.
아마도 같은 "Enemy" Tag를 갖고있는 몬스터는 위에서 밟으면 피격당해 죽게되지만, 가시함정은 피격당할 수 없는 타일맵 속성 오브젝트이기 때문에 관련 애니메이션이나 기타 로직이 실행될 수 없기 때문에 Null Reference가 뜬 것으로 보인다.
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Enemy")
{
//Attack
if (rigid.velocity.y < 0 && transform.position.y > collision.transform.position.y)
OnAttack(collision.transform);
//Damaged
else
OnPlayerDamaged(collision.transform.position);
}
else if (collision.gameObject.tag == "Spikes")
OnPlayerDamaged(collision.transform.position);
}
따라서 가시함정의 경우 태그를 "Spikes"로 설정하고, 어느 방향에서 플레이어와 접촉하든 플레이어는 무조건 피격만 되도록 설정했다.
*다른 Scene에도 적절히 설정해줘야함.
9. Retry 버튼 눌렀을 때 Stage1 처음부터 재시작 안하는 문제
(7번 해결함으로서 해결완료)
10. 몬스터 피격 시(죽을 때) 피격 애니메이션 구현 / 플레이어 죽을 때 애니메이션 종료or 피격애니메이션 구현
//GamerManager
public void Retry()
{
Invoke("TimeStop", 1.5f);
UIRestart.SetActive(true);
}
void TimeStop()
{
Time.timeScale = 0;
}
//PlayerMove
public void OnDie()
{
anime.SetBool("IsWalk", false);
anime.SetBool("IsJump", false);
anime.SetBool("IsDie", true);
//Sprite Alpha
spriteRenderer.color = new Color(1, 1, 1, 0.4f);
//Sprite Flip Y
spriteRenderer.flipY = true;
//Collider Disable
capsuleCollider.enabled = false;
//Die Effect Jump
rigid.AddForce(Vector2.up * 5, ForceMode2D.Impulse);
}
플레이어 죽음 애니메이션을 추가하고, 의도에 맞게 프레임을 조정한 후, 반복되면 안되니까 Set Loop Time을 체크 해제하고, Animator에서 Transition 화살표를 설정했다.
그리고 GameManager에서는 플레이어가 죽은 뒤 바로 멈추지 않고 1.5초 정도 Die 애니메이션이 재생되다가 시간이 멈추고 Retry UI 버튼이 활성화되도록 했다. [이걸 코루틴으로 바꿀 수도 있을 것 같은데 나중에 추가해볼 생각]
몬스터가 죽을 때 모션은 추가하기가 어려웠다. 왜냐하면, Enemy_Idle 애니메이션과 Enemy_Walk 애니메이션을 전환시키는 매개변수가 0과 같은지 여부에 따라 결정되는 Int형 매개변수 WalkSpeed인데, 여기에 Bool형의 매개변수 등을 추가한다고 해서 죽을 때 이전 모션 Idle 또는 Walk)을 비활성화할 수 없기 때문이다.
따라서 애니메이션 매개변수로 Int형 또는 Float형을 사용하는 경우에, Trigger 애니메이션이 아닌 state 애니메이션이 3개 이상 있을 경우에, 숫자값을 같은지 다른지 여부로 판단하는 것은 무리가 있을 것 같다.
따라서 어떤 Int형 매개변수를 둔다면 구간을 설정해서, 그 값이 1과 같을 때 / 2와 같을 때 / 3과 같을 때 혹은 보다 작을때 vs 보다 클때를 기준으로 두는 것이 바람직할 것 같다.
아니면 Bool형으로 매개변수를 설정하거나..
public void OnMonsterDamaged()
{
anime.SetBool("MonsterDie", true);
//Sprite Alpha
spriteRenderer.color = new Color(1, 1, 1, 0.4f);
//Sprite Flip Y
spriteRenderer.flipY = true;
//Collider Disable
CircleCollider.enabled = false;
//Die Effect Jump
rigid.AddForce(Vector2.up * 5, ForceMode2D.Impulse);
//Destroy
Invoke("Deavtive", 5);
}
근데 생각외로 잘 작동했다. 스크립트 저장이 안됐거나 버그였던 것 같다. 숫자형 매개변수와 Bool형 매개변수를 함께 사용하는 것은 이 방법이 괜찮을지 더 좋은 방법이 있을지 다시한번 생각해봐야겠다.
11. 경사로 올라갈때 평지에서와 속도 일정하게 유지하고 싶다.
Ground Material의 마찰력(Friction) 0.025, Player 오브젝트의 RigidBody 2D 컴포넌트의 중력크기(Gravity Scale) 3, 바람저항(Angular Drag) 0.05, 질량(Mass) 1, PlayerMove 스크립트 컴포넌트의 MaxSpeed 값 4.5로 설정했더니 그래도 자연스럽게 경사로를 올라갈 수 있었다.
완전히 마찰력을 0으로 두면, 캐릭터가 착지 후 빙판처럼 미끄러지는 문제도 있어서 적은 값이라도 설정해주는 것이 좋았고, Max Speed 값과 Gravity Scale 및 Angular Drag 값을 적절히 분배해서 설정하는 것이 현재로서 할 수 있는 최선의 방법 같다.
12. 가끔씩 시작할 때 몬스터가 가만히 있어서 Idle 상태로 유지되는 문제 (움직여야함)
void Think()
{
NextMove = Random.Range(-1, 2);
//Sprite Animation
anime.SetInteger("WalkSpeed", NextMove);
//Flip Sprite
if (NextMove != 0)
spriteRenderer.flipX = NextMove == 1;
//재귀함수 : 맨 마지막에 작성
Invoke("Think", NextThinkTime);
}
캐릭터의 이동방향으로 빔을 쏘기 위해 Start() 함수 내에 NextMove = Random.Range();를 입력했는데, Think() 함수에도 NextMove를 값을 계속 랜덤으로 대입할 수 있도록 입력해주었어야했다.
왜냐하면 처음 이동방향이 1이나 -1이면 Think의 모든 로직이 수행되고 애니메이션도 바뀌고, FixedUpdate()의 이동방향(1or-1)의 반대값으로 바꿔주고 적절히 flipX가 활성화될 수 있었다. 하지만 처음부터 이동방향이 0이라면 애니메이션도 idle 고정이고, 방향도 안바뀌고 NextMove 값은 업데이트되질 않기 때문에 NextMove 값도 업데이트될 수 있게 Think() 함수에도 작성해주었어야했다.
이상한 부분
health가 1일때 -> PlayerReposition()함수 실행: (-8, 1, -1)위치 이동, 플레이어 속도 0
public void HealthDown()
{
if (health > 0)
{
health--;
UIhealth[health].color = new Color(1, 0, 0, 0.2f);
}
else if (health == 0)
{
player.OnDie()
Invoke("Retry", 1);
}
}
이렇게 health가 0 이상이면 살아있다는 것이니까, health를 피1 감소시키고 health가 0일때는 죽었으므로 OnDie()를 호출하고 1초 뒤 시간 정지, Retry 버튼을 활성화시키는 Retry()함수를 호출하는 식으로 코드를 줄여서 작성할 수 있을 것 같다.
있으면 좋겠는 것: 에디터 상에서 어떠한 stage가 체크되어있든 간에 게임시작하면 처음에는 stage1만 활성화 / 플레이어가 어디에 위치해있든 간에 특정 지점(체크포인트)에서 스폰
https://github.com/pix-lin/Unity_2d-platformer.git
아직 코드적으로 코루틴 등 개선해야할 부분은 많고, 또 구현해보고싶은 기능들도 많지만 당장 할 수 있는 부분은 최대한 수정하고 개선해서 첫 유니티 프로젝트를 끝마쳤다. 앞으로도 계속 이렇게 부족한 부분은 검색해가며 학습하여 프로젝트를 발전시킨다면 너무 좋을 것 같다.
(해당 프로젝트는 유튜브 골드메탈님의 유니티 기초강좌를 따라 진행하고 있습니다.)
출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared
'개발 > 유니티' 카테고리의 다른 글
탑다운 2d RPG(2) - 쯔꾸르식 액션 구현하기 (0) | 2024.01.30 |
---|---|
탑다운 2d RPG(1) - 도트 타일맵으로 쉽게 준비하기 (0) | 2024.01.29 |
유니티 코루틴(Coroutine) / 서브루틴(Sub Routine) +α (0) | 2024.01.27 |
TextMeshPro 활성화 /비활성화 (ft. SetActive() vs .enabled) (0) | 2024.01.26 |
유니티 TextMeshPro(TMP) UI 래퍼런스 할당 (0) | 2024.01.26 |