📁PROJECT/❄2021 APPS EXHIBITION

2021 APPS EXHIBITION 후기

coldNoodlePigeon 2021. 12. 24.
 

GitHub - Sookmyung-APPS/Make-My-Wish-Come-True: 2021 유니티팀 김진영

2021 유니티팀 김진영 . Contribute to Sookmyung-APPS/Make-My-Wish-Come-True development by creating an account on GitHub.

github.com

(어? 코드가 안보여요! 나도 안다. 커밋을 안하고 싶어서 안한 것이 아니다. 그 이유는 차차 설명하겠다.) 

 

 

 

 

처음으로 혼자서 뭐라도 해보려고 발악했던 개인 유니티 프로젝트. 

개발한지는 시간이 조금 지났지만(....)  느꼈던 점, 부족했거나 아쉬웠던 점, 개선할 점 등에 대해 짧게 기록해두려고 한다. 아무래도 일기 형식으로 쓸 예정이라 ~다 형식으로 글을 쓰려고 한다. 그래서 조금 딱딱해보일 수도 있다. 유의바랍니다. 

 


1. 개발 초기 

처음 아이디어는 환경 문제의 심각성을 알릴 수 있는 미니게임들이 여러가지 들어간 유니티 2d 게임을 제작하는 것이 목표였다. 그래서 처음에는 가장 기본적인 미니게임들을 3 stage로 구성하여서 챕터별로 넣을 예정이었다. 그래서 일단 처음부터 필요한 무료 asset을 다운 받고 여러 유니티 기초 강의들을 찾아 수강하였다. 

 

2D 게임을 만들고 싶어서 유튜브 골드메탈님의 Unity 강의들을 찾아 들었는데 강의력도 좋으셔서 참 많은 도움이 되었다. 

 

먼저 게임에 필요한 에셋들을 하나씩 만들기로 결정했다. 가장 먼저 플레이어, 아이템, npc를 piskel이라는 도트를 찍을 수 있게 제공해주는 무료 사이트에서 그렸다. 

 

직접 도트로 찍은 player(눈송)  

도트로 움직임을 직접 표현하느라 힘들었다. 이것도 유튜브에서 이것저것 자료를 찾아보며 참고하면서 찍었다. 

 

플라스틱 오브젝트들. 
장애물 도트
알바트로스 npc 오브젝트. 

 

그 다음으로는 전체적인 씬들과 맵을 구성해주었다. 

 

 

배경과 타일맵 같은 경우 유니티에서 제공해주는 무료 에셋을 사용하였다. 전체적인 기본 틀을 잡아놓고 하나하나 구현을 시작했다. 

 


2. 개발 중기 

 

아래는 플레이어의 움직임 코드. 들었던 강의의 코드를 많이 참고했다. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class PlayerMove : MonoBehaviour
{

    public float maxSpeed;
    public float jumpPower;
    Rigidbody2D rigid;
    SpriteRenderer spriteRenderer;
    Animator anim;
    GameObject scanObject;

    void Awake()
    {
        rigid = GetComponent<Rigidbody2D>();
        spriteRenderer = GetComponent<SpriteRenderer>();
        anim = GetComponent<Animator>();
    }

    void Update()
    {
        moving();
    }

   
    void FixedUpdate()
    {
        float h = Input.GetAxisRaw("Horizontal");
        rigid.AddForce(Vector2.right * h, ForceMode2D.Impulse);
        if (rigid.velocity.x > maxSpeed)
            rigid.velocity = new Vector2(maxSpeed,rigid.velocity.y);
        else if(rigid.velocity.x < maxSpeed*(-1))
            rigid.velocity = new Vector2(maxSpeed*(-1), rigid.velocity.y);


        landing();

        //미니게임 실행 
        if (GameManager.Instance.stopTrigger)
        {
            moving();

            if (GameManager.Instance.PlasticScore >= 15)
            {
                GameManager.Instance.GameClear();
                SceneManager.LoadScene("04");
            }
        }

        if (!GameManager.Instance.stopTrigger)
        {

        }

        //오브젝트 조사 
        
        Debug.DrawRay(rigid.position,Vector3.right, new Color(0, 1, 0));
        RaycastHit2D rayHit = Physics2D.Raycast(rigid.position,new Vector3(1,0,0),0.48f,LayerMask.GetMask("Object"));

        if (rayHit.collider != null)
        {
            scanObject = rayHit.collider.gameObject;
        }
        else
            scanObject = null;
       


    }


    void landing()
    {
        if (rigid.velocity.y < 0)
        {
            Debug.DrawRay(rigid.position, Vector3.down, new Color(0, 1, 0));
            RaycastHit2D rayHit = Physics2D.Raycast(rigid.position, Vector3.down, 1, LayerMask.GetMask("Platform"));

            if (rayHit.collider != null)
            {
                if (rayHit.distance < 0.5f)
                    anim.SetBool("isJumping", false);
            }
        }
    }

    void moving()
    {
        if (Input.GetButtonDown("Jump") && !anim.GetBool("isJumping"))
        {
            rigid.AddForce(Vector2.up * jumpPower, ForceMode2D.Impulse);
            anim.SetBool("isJumping", true);
        }

        //오브젝트 스캔
        if (Input.GetMouseButtonDown(0) && scanObject != null)
            GameManager.Instance.Action(scanObject);




        if (Input.GetButtonUp("Horizontal"))
        {
            rigid.velocity = new Vector2(rigid.velocity.normalized.x * 0.5f, rigid.velocity.y);
        }

        if (Input.GetButton("Horizontal"))
        {
            spriteRenderer.flipX = Input.GetAxisRaw("Horizontal") == -1;
        }

        if (rigid.velocity.normalized.x == 0)
        {
            anim.SetBool("isWalking", false);
        }
        else
        {
            anim.SetBool("isWalking", true);
        }
    }


}

 

 

플레이어의 움직임까지 구현하고 미니게임을 하나하나 구현하는 과정에서 문제가 많이 생겼었다. 

처음보는 버그에.. 마음대로 움직이지 않는 유니티 때문에 이걸 해결하는데 시간을 참 많이 썼었다. 전시회까지의 시간이 촉박해서 결국 3 stage의 미니게임들로 제작하기로 했던 계획을 대폭 수정해서 1개의 미니게임으로 구성된 게임이라도 제대로 완성하자는 계획으로 했다. 계획을 수정하면서 아직 내 능력이 많이 부족하다는 것을 참 절실히도 느꼈었다. 

 

 아래의 코드는 대화스크립트이다. 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TalkManager : MonoBehaviour
{
    Dictionary<int, string[]> talkData;
    Dictionary<int, Sprite> portraitData;

    public Sprite[] portraitArr;

    void Awake()
    {
        talkData = new Dictionary<int, string[]>();
        portraitData = new Dictionary<int, Sprite>();
        GenerateData();
    }

    void GenerateData()
    {
        talkData.Add(1000,new string[] {"안녕?:0","드디어 눈을 떴구나.:1","당신은 누구고..여긴 어디죠?:4","난 요정. 소원을 들어주는 요정이야!:0","저 빨리 좀 집에 돌려보내주세요! 과제 제출해야 하는데..:4","대신..소원을 너가 이뤄준다면, 내가 집으로 돌려보내줄게.:1","지금 급하게 소원을 빈 친구가 있거든. 부탁해!:0","예? 어휴..알겠어요.:4","어디로 가면 되는거죠?:4","오른쪽으로 쭉 가면 있을거야. 건투를 빌게!:2"});
        
        talkData.Add(2000, new string[] {"아이고 아파라..배가 너무 아파:6", "배가 너무 아파...:6", "헉 괜찮아요?:5","내 배에 뭔가가 있는 것 같아. 뭔가를 좀..꺼내줄 수 있니?:6","제발 부탁이야..:6","걱정 마세요! 제가 해결해드릴게요!:5","정말 고마워...:6","위에 버튼을 누르면 내 뱃속으로 들어갈 수 있을거야:6","너무 걱정마세요! 그럼 해결해드리러 가볼게요!:5","정말 고마워..부탁할게.:6" });



        portraitData.Add(1000 + 0,portraitArr[0]);
        portraitData.Add(1000 + 1, portraitArr[1]);
        portraitData.Add(1000 + 2, portraitArr[2]);
        portraitData.Add(1000 + 3, portraitArr[3]);
        portraitData.Add(1000 + 4, portraitArr[4]);

        portraitData.Add(2000 + 5, portraitArr[5]);
        portraitData.Add(2000 + 6, portraitArr[6]);


    }

    public string GetTalk(int id,int talkIndex)
    {
        if (talkIndex == talkData[id].Length)
            return null;
        else
            return talkData[id][talkIndex];
    }

    public Sprite GetPortrait(int id,int portraitIndex)
    {
        return portraitData[id + portraitIndex];
    }
}

 

오브젝트에 id를 지정해주기 위해 만든 스크립트 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ObjData : MonoBehaviour
{
    public int id;
    public bool isNpc;


}

 

plastic 오브젝트를 먹어야 점수가 오르도록 하기 위해 만든 스크립트.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class plastic : MonoBehaviour
{


    void Start()
    {

    }


    void Update()
    {
        
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Player")
        {
            GameManager.Instance.SScore();
            EffectManager.instance.PlaySound();
            Destroy(this.gameObject);
        }

        if (collision.gameObject.tag == "Ground")
        {
            Destroy(this.gameObject);
        }

    }

}

poop은 말그대로 피해야하는 장애물이기 떄문에 닿으면 바로 게임이 오버되도록 하는 스크립트를 만들었다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class poop : MonoBehaviour
{
    
    void Start()
    {
        
    }

    
    void Update()
    {
        
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if(collision.gameObject.tag == "Ground")
        {
            GameManager.Instance.Score();
            Destroy(this.gameObject);
        }

        if(collision.gameObject.tag == "Player")
        {
            GameManager.Instance.GameOver();
            EffectManager2.instance.PlaySound2();
            Destroy(this.gameObject);
        }

    }

}

 

 

가장 중요했던 GameManager 스크립트.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    
    //플라스틱 오브젝 피하기 

    private static GameManager _instance;


    public static GameManager Instance
    {
        get
        {
            if(_instance == null)
            {
                _instance = FindObjectOfType<GameManager>();
            }
            return _instance;
        }
    }

    [SerializeField]
    private GameObject poop;

    [SerializeField]
    public List<GameObject> plastic;
    //private GameObject plastic;

    private int score;
    public int PlasticScore;

    [SerializeField]
    private TextMeshProUGUI scoreTxt;

    [SerializeField]
    private TextMeshProUGUI PlasticScoreTxt;

    [SerializeField]
    private GameObject panel;

    void Start()
    {
       
    }
    void Update()
    {

    }

    public bool stopTrigger = true;

    public void GameOver()
    {
        stopTrigger = false;
        StopCoroutine(CreatepoopRoutine());

        StopCoroutine(CreatePlasticRoutine());

        panel.SetActive(true);
    }
    
    public void GameClear()
    {
        stopTrigger = false;
        StopCoroutine(CreatepoopRoutine());

        StopCoroutine(CreatePlasticRoutine());
    }
 

    public void GameStart()
    {
        score = 0;
        PlasticScore=0;
        PlasticScoreTxt.text= "Plastic: "+PlasticScore;
        scoreTxt.text = "Score: " + score;
        stopTrigger = true;
        StartCoroutine(CreatepoopRoutine());

        StartCoroutine(CreatePlasticRoutine());


        panel.SetActive(false);


    }

    public void Score()
    {
        if(stopTrigger)
            score++;
        scoreTxt.text = "Score:" + score;
    }

    
     public void SScore()
    {
        if (stopTrigger)
            PlasticScore++;
        PlasticScoreTxt.text = "Plastic: " + PlasticScore;
    }
     
     
    IEnumerator CreatepoopRoutine()
    {
        while (stopTrigger)
        {
            CreatePoop();
            yield return new WaitForSeconds(0.3f);
        }
    }
    
    IEnumerator CreatePlasticRoutine()
    {
        while (stopTrigger)
        {
            CreatePlastic();
            yield return new WaitForSeconds(1.0f);
        }
    }


    private void CreatePlastic()
    {
        Vector3 pos2 = Camera.main.ViewportToWorldPoint(new Vector3(UnityEngine.Random.Range(0.0f, 1.0f),1.1f, 0));
        pos2.z = 0.0f;
        Instantiate(plastic[Random.Range(0,plastic.Count)],pos2,Quaternion.identity);
    }


    private void CreatePoop()
    {
        Vector3 pos = Camera.main.ViewportToWorldPoint(new Vector3(UnityEngine.Random.Range(0.0f, 1.0f), 1.1f, 0));
        pos.z = 0.0f;
        Instantiate(poop, pos, Quaternion.identity);
    }
    


    
    //대화창 

    public TalkManager talkManager;
 

    public GameObject talkPanel;
    public Text talkText;

    public GameObject scanObject;

    public bool isAction;
    public int talkIndex;
    public Image portraitImg;


    public void Action(GameObject scanObj)
    {
        scanObject = scanObj;
        ObjData objData = scanObject.GetComponent<ObjData>();
        Talk(objData.id, objData.isNpc);
        talkPanel.SetActive(isAction);
    }


    void Talk(int id, bool isNpc)
    {
        string talkData = talkManager.GetTalk(id, talkIndex);
        if(talkData == null)
        {
            isAction = false;
            talkIndex = 0;
            return;
        }


        if (isNpc)
        {
            talkText.text = talkData.Split(':')[0];
            portraitImg.sprite = talkManager.GetPortrait(id, int.Parse(talkData.Split(':')[1]));
            portraitImg.color = new Color(1, 1, 1, 1);
        }
        else
        {
            talkText.text = talkData;
            portraitImg.color = new Color(1, 1, 1, 0);
        }

        isAction = true;
        talkIndex ++;
    }

}

 

게임의 중요한 효과음을 넣기 위한 스크립트.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AudioManager : MonoBehaviour
{
    void Start()
    {
        DontDestroyOnLoad(transform.gameObject);   
    }


}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EffectManager : MonoBehaviour
{
    public AudioClip e;
    AudioSource myAudio;
    public static EffectManager instance;
    void Awake()
    {
        if (EffectManager.instance == null)
        {
            EffectManager.instance = this;
        }
    }

    // Update is called once per frame
    void Start()
    {
        myAudio = GetComponent<AudioSource>();
    }

    public void PlaySound()
    {
        myAudio.PlayOneShot(e);
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EffectManager2 : MonoBehaviour
{
    public AudioClip e2;
    AudioSource myAudio2;
    public static EffectManager2 instance;
    void Awake()
    {
        if (EffectManager2.instance == null)
        {
            EffectManager2.instance = this;
        }
    }

    void Start()
    {
        myAudio2 = GetComponent<AudioSource>();
    }

    public void PlaySound2()
    {
        myAudio2.PlayOneShot(e2);
    }
}

 

 

c#은 아직 모르는것도 많고 구현 능력 부족때문에 구글링하면서 여러가지 참고했었다. 그렇게 코드를 완성하고 씬 연결도 마무리짓고 제대로 게임이 unity에서 굴러가는 걸 확인후 이제 build만을 남겨두고 있었다. 

 


3. 개발 마무리. 그러나..

 

너무너무 중대한 실수를 했다. 

게임의 해상도를 고려하지 않고...

그냥 임의로 main camera 사이즈를 마음대로 해서 게임을 만들어버린것이다. 

곧 전시회 마감일은 다가오고.. 수정할 시간은 부족해서 결국 내가 초기에 설정해주었던 main camera size에 맞추어서 해상도가 고정되도록 하고 게임을 빌드했다. 정말 잘 알고 있는 사실이지만.. 게임의 경우 사용자 기기에 따라 정말 다양한 해상도를 지원해줘야하기 때문에 실제로 게임을 빌드할때는 이 사실을 정말 잘 고려하고 있어야한다. 

unity로 개발하면서 맵을 구성할때 각각의 해상도에서 어떻게 화면이 보여지는지를 확인하며 개발해야한다. 

 

그렇게 빌드를 성공하고 github에 commit만을 남겨두고 있었다. 내가 스스로 공부하면서 만든 코드들.. asset들.. 이런것들을 github에 올릴 생각을 하니 뿌듯했다. 비록 실력은 아주아주 부족한 수준이지만 그래도 나름 뭔가를 만들었다는 사실 자체가 뿌듯했다.

 

그런데 여기서 문제가 발생한다. 

 

github를 처음 사용해보는거라 너무 미숙했었고, 파일들을 commit하는데 수정해야할 것이 있어서 잠시...올렸던 파일들을 discard 했는데

슝..~

 

그 모든 파일들이 분해되어서 휴지통으로 간 것이다. 

 

컴공이지만 컴맹이었던 나는 부랴부랴 복원을 했지만 파일들이 분리되어서 복원이 되는 바람에 손을 쓸 수가 없었다. 

결국 나는 눈물을 머금으며 github에 빌드 파일밖에 올리지 못했다.

그것이 지금 내 github에 코드가 하나로 올라와있지 않게된 슬픈 이유이다. 

 

나중에 협력하여 프로젝트를 진행한다거나 할때는 github에 미리 익숙해져야 있음을 아주아주 절실히 느꼈다. 개인 프로젝트라 다행이지 만약 이게 팀 프로젝트라고 생각해봐라. 정말 아찔할 것이다. 

 

지금 윗글에서 올린 코드는 내가 휴지통을 하나하나 뒤져가면서 찾은 코드들을 복원해놓은 것이다. (.....) 

찾을때 정말 눈물이 났다. 

다음에는 절대 이런 실수를 하지 않겠다. 정말 절실히 배웠다. 

 

 


4. 후기 

 

원하던 만큼의 결과물이 나오지 않았지만 일단 무언가를 만들어봤다는 사실에 더 집중하기로 했다. 아직 능력은 많이 부족하고 아는 것도 부족하지만 이것도 하나하나 쌓여서 다 경험이 되는 게 아닐까? 

 

내 능력을 돌아보고 많은 삽질들을 경험하면서 여러모로 한걸음 성장할 수 있었던 것 같다. 

다음에는 좀 더 멋진 결과물을 만들어보기 위해 더 열심히 공부해야지. 

 

아래 링크는 선배님들이 만드신

완전 킹왕짱 멋있는 전시회 링크이다. 

 

https://sookmyung-apps.github.io/#/ 

 

React App

 

sookmyung-apps.github.io

 

플레이 영상들. 모두 멋진 작품이니까 한번씩 봐주세요. 

 

https://www.youtube.com/channel/UCmlrj2FDcGhaYW1dERslImg/videos 

 

숙명APPS

 

www.youtube.com

 

 

아자아자 파이팅

댓글