포트폴리오/개인 프로젝트

[개인 프로젝트] NEON

이름?없음 2024. 3. 5. 00:48
반응형
반응형

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학기 유니티에 익숙하지 않았기 때문에 작성한 코드에 아쉬운 부분이 많았다.

기능 구현 하나하나에 너무 많은 시간이 소요되었다.

 

프로젝트를 통해 느낀 점

유니티를 더 공부해야겠다는 생각이 들었다. 투자한 시간 대비하여 퀄리티가 높지 않아 아쉬웠다. 하지만 목표한 기능을 전부 구현한 것에 대해 뿌듯했다.

반응형