2022년 1학년 1학기 개인프로젝트 포트폴리오
깃허브 : https://github.com/dkdkdsa/Neon_Main
다운로드 : https://drive.google.com/file/d/1qXcL5OUwqZwDZV0N7TYblIHUfQwd_Xad/view?usp=drive_link
동영상 : https://youtu.be/dr4TYlvKhr8
NEON.zip
drive.google.com
1. 게임 개요
설명 : 여러 장애물을 피해 맵을 최대한 빠른 시간 안에 클리어해야 하는 게임
기획 의도 : 바운스볼 마리오등 여러 플렛포머 게임을 해보다가 이런 종류에 게임에 타임어택요소를 넣으면 재미있겠다고 생각해서 기획하게되었다
2. 장르 : 플렛포머
3. 개발 전 구상
벽 타기
속도감을 살린 조작감
네온 느낌을 잘 살린 맵 디자인
4. 구현
플레이어 움직임
Rigidbody2D를 사용하여 플레이어의 움직임과 벽타기를 구현하였다

벽타기

코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
[SerializeField] private float speed;
[SerializeField] private float jumpPower;
[SerializeField] private float slidePower;
[SerializeField] private float wallJumpPower;
[SerializeField] private float daleyTime;
[SerializeField] private KeyCode jumpKeyCode;
[SerializeField] private Timer timer;
[SerializeField] private GameObject dieEffect;
[SerializeField] private SpriteRenderer spriteRenderer;
[SerializeField] private Vector2 boxSize;
[SerializeField] private Vector2 parkourBoxSize;
private bool load;
private bool isWall;
private bool isWallJumping;
private string dir;
private Collider2D isGround;
private Collider2D wall;
private Collider2D moveWall;
private Rigidbody2D playerRigidbody;
private string playerDir;
public string PlayerDir => playerDir;
void Awake()
{
playerRigidbody = GetComponent<Rigidbody2D>();
}
void Update()
{
if(StageLoader.isLoading == false)
{
if (Input.GetKeyDown(KeyCode.Escape))
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#endif
Application.Quit();
}
//땅 감지
isGround = Physics2D.OverlapBox(transform.position, boxSize, 0, LayerMask.GetMask("Ground"));
//벽 감지
wall = Physics2D.OverlapBox(transform.position, parkourBoxSize, 0, LayerMask.GetMask("Ground"));
//움직이는 벽 감지
moveWall = Physics2D.OverlapBox(new Vector2(transform.position.x, transform.position.y - 0.35f), new Vector2(0.9f, 0.3f), 0, LayerMask.GetMask("MoveGround"));
Parkour();
MoveGround();
Die();
Clear();
if(isWall == false)
{
Jump();
}
}
}
//벽이 뚫리는거 방지
void FixedUpdate()
{
if(StageLoader.isLoading == false)
{
Move();
}
}
//움직임
private void Move()
{
float moveX = 0;
if (isWallJumping == false && isWall == false) moveX = Input.GetAxisRaw("Horizontal");
playerDir = moveX switch
{
1 => "R",
-1 => "L",
_ => playerDir,
};
if ((isWallJumping == true || isWall == true) && Input.GetKey(KeyCode.LeftArrow) && dir == "L") moveX = -1;
if ((isWallJumping == true || isWall == true) && Input.GetKey(KeyCode.RightArrow) && dir == "R") moveX = 1;
transform.Translate(new Vector2(moveX, 0) * speed * Time.deltaTime);
}
//움직이는 바닥에 닿으면 같이 움직이게
private void MoveGround()
{
if(moveWall == true)
{
transform.SetParent(moveWall.transform);
}
else
{
transform.SetParent(null);
}
}
//점프
private void Jump()
{
if((isGround == true || moveWall == true) && Input.GetKeyDown(jumpKeyCode))
{
playerRigidbody.AddForce(Vector2.up * jumpPower, ForceMode2D.Impulse);
}
}
//파쿠르
private void Parkour()
{
if (wall == true && isWall == false)
{
playerRigidbody.velocity = Vector2.zero;
isWallJumping = false;
StopCoroutine("delay");
}
if(wall == true && isGround == false)
{
isWall = true;
playerRigidbody.gravityScale = 0;
transform.Translate(Vector2.down * slidePower * Time.deltaTime);
if(wall.transform.position.x > transform.position.x) dir = "L";
else if(wall.transform.position.x < transform.position.x) dir = "R";
if (Input.GetKeyDown(jumpKeyCode))
{
if(wall.transform.position.x > transform.position.x)
{
playerRigidbody.AddForce(new Vector2(-0.5f, 1.7f) * wallJumpPower, ForceMode2D.Impulse);
StartCoroutine("delay");
}
else if(wall.transform.position.x < transform.position.x)
{
playerRigidbody.AddForce(new Vector2(0.5f, 1.7f) * wallJumpPower, ForceMode2D.Impulse);
StartCoroutine("delay");
}
}
}
else if(wall == true)
{
isWall = true;
if (wall.transform.position.x > transform.position.x) dir = "L";
else if (wall.transform.position.x < transform.position.x) dir = "R";
}
else
{
isWall = false;
playerRigidbody.gravityScale = 2;
}
}
//죽는거
private void Die()
{
if(Physics2D.OverlapBox(transform.position, new Vector2(1, 1), 0, LayerMask.GetMask("Trap")) || Physics2D.OverlapBox(transform.position, new Vector2(1, 1), 0, LayerMask.GetMask("Enemy")))
{
StageLoader.isLoading = true;
Instantiate(dieEffect, transform.position, Quaternion.identity);
spriteRenderer.enabled = false;
if (load == false)
{
timer.Die();
load = true;
}
Debug.Log("Die");
}
}
//스테이지 클리어
private void Clear()
{
if(Physics2D.OverlapBox(transform.position, new Vector2(1, 1), 0, LayerMask.GetMask("Clear")))
{
if(load == false)
{
timer.TiemSet();
load = true;
}
Debug.Log("Clear");
}
}
//벽점프 움직임 제한 딜레이
IEnumerator delay()
{
isWallJumping = true;
yield return new WaitForSeconds(daleyTime);
isWallJumping = false;
}
}
플레이어 사격
Input을 이용하여 플레이어 총알 발사를 구현하였다

코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerSkill : MonoBehaviour
{
[SerializeField] private float coolTime;
[SerializeField] private KeyCode fireKey;
[SerializeField] private GameObject bullet;
private bool isFireCoolTime;
private Quaternion bulletRotate;
private Player player;
void Awake()
{
player = GetComponent<Player>();
}
// Update is called once per frame
void Update()
{
Fire();
}
//플레이어 총알 발사(Q키)
private void Fire()
{
if (Input.GetKey(fireKey) && isFireCoolTime == false)
{
bulletRotate = player.PlayerDir switch
{
"R" => Quaternion.Euler(new Vector3(0, 0, 0)),
"L" => Quaternion.Euler(new Vector3(0, 0, 180)),
_ => Quaternion.Euler(new Vector3(0, 0, 0)),
};
Instantiate(bullet, transform.position, bulletRotate);
StartCoroutine(CoolTime());
}
IEnumerator CoolTime()
{
isFireCoolTime = true;
yield return new WaitForSeconds(coolTime);
isFireCoolTime = false;
}
}
}
PlayerPrefs를 사용하여 배스트 타임 시스템을 구현하였다

코드
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using TMPro;
using UnityEngine.SceneManagement;
using DG.Tweening;
using UnityEngine.UI;
public class Timer : MonoBehaviour
{
[SerializeField] private Image image;
[SerializeField] private float duration;
[SerializeField] private TextMeshProUGUI loadingText;
[SerializeField] private Vector2 moveDir;
[SerializeField] private Ease ease;
[SerializeField] AudioSource playSound;
private bool isClear;
private float bestClearTime;
private float time;
public static bool start;
// Update is called once per frame
void Update()
{
Debug.Log(time);
if(start == true)
{
if(isClear == false)
{
time += Time.deltaTime;
}
}
}
public void TiemSet()
{
isClear = true;
playSound.Pause();
StageLoader.isLoading = true;
int i = PlayerPrefs.GetInt("StageNum".ToString());
bestClearTime = PlayerPrefs.GetFloat(i.ToString());
loadingText.text = "Loading";
image.DOFade(1, 0.3f);
if (bestClearTime == 0)
{
bestClearTime = time;
Debug.Log(1);
PlayerPrefs.SetFloat("BestClearTime", bestClearTime);
PlayerPrefs.SetFloat("ThisClearTime", time);
PlayerPrefs.SetInt("isGameOver", 1);
Sequence sequence = DOTween.Sequence()
.Append(loadingText.transform.DOMove(new Vector2(loadingText.transform.position.x - moveDir.x, loadingText.transform.position.y - moveDir.y), duration)).SetEase(ease)
.OnComplete(() =>
{
StartCoroutine(Loading());
});
}
else if(bestClearTime > time)
{
bestClearTime = time;
Debug.Log(2);
PlayerPrefs.SetFloat("BestClearTime", bestClearTime);
PlayerPrefs.SetFloat("ThisClearTime", time);
PlayerPrefs.SetInt("isGameOver", 1);
Sequence sequence = DOTween.Sequence()
.Append(loadingText.transform.DOMove(new Vector2(loadingText.transform.position.x - moveDir.x, loadingText.transform.position.y - moveDir.y), duration)).SetEase(ease)
.OnComplete(() =>
{
StartCoroutine(Loading());
});
}
else
{
PlayerPrefs.SetFloat("BestClearTime", bestClearTime);
PlayerPrefs.SetFloat("ThisClearTime", time);
PlayerPrefs.SetInt("isGameOver", 1);
Debug.Log(3);
Sequence sequence = DOTween.Sequence()
.Append(loadingText.transform.DOMove(new Vector2(loadingText.transform.position.x - moveDir.x, loadingText.transform.position.y - moveDir.y), duration)).SetEase(ease)
.OnComplete(() =>
{
StartCoroutine(Loading());
});
}
}
public void Die()
{
isClear = true;
StageLoader.isLoading = true;
PlayerPrefs.SetFloat("ThisClearTime", time);
PlayerPrefs.SetInt("isGameOver", 0);
loadingText.text = "Loading";
image.DOFade(1, 0.3f);
playSound.Pause();
Sequence sequence = DOTween.Sequence()
.Append(loadingText.transform.DOMove(new Vector2(loadingText.transform.position.x - moveDir.x, loadingText.transform.position.y - moveDir.y), duration)).SetEase(ease)
.OnComplete(() =>
{
StartCoroutine(Loading());
});
}
IEnumerator Loading()
{
for(int i = 0; i < 1; i++)
{
for(int j = 0; j < 3; j++)
{
loadingText.text += ".";
yield return new WaitForSeconds(0.5f);
}
loadingText.text = "Loading";
yield return new WaitForSeconds(0.5f);
}
Sequence sequence = DOTween.Sequence()
.Append(loadingText.transform.DOMove(new Vector2(loadingText.transform.position.x + moveDir.x, loadingText.transform.position.y + moveDir.y), duration)).SetEase(ease)
.OnComplete(() =>
{
start = false;
SceneManager.LoadScene("Stage_End");
});
}
}
프로젝트에서의 문제점
1학년 1학기 유니티에 익숙하지 않았기 때문에 작성한 코드에 아쉬운 부분이 많았다.
기능 구현 하나하나에 너무 많은 시간이 소요되었다.
프로젝트를 통해 느낀 점
유니티를 더 공부해야겠다는 생각이 들었다. 투자한 시간 대비하여 퀄리티가 높지 않아 아쉬웠다. 하지만 목표한 기능을 전부 구현한 것에 대해 뿌듯했다.
'포트폴리오 > 개인 프로젝트' 카테고리의 다른 글
[개인 프로젝트] FAED (0) | 2024.04.29 |
---|---|
[개인 프로젝트] 용사는 더 이상 적을 막을 수 없어 (0) | 2024.03.08 |
[개인 프로젝트] DESERT (2) | 2024.03.05 |