해피류 개발이야기 두번째 토끼는 점프중



두번째 게임을 공개합니다. 이번에는 2D 게임에 도전해봤는데, 토끼가 지구에 떨어졌는데, 달나라까지 점프해서 올라가는 내용입니다. 그냥 점프하는 게임 만들어보고 싶었고 스토리는 어거지로 붙여 넣은거 표시나지요?


아직 C#이 적응이 안되네요. 좀 고급 문법도 사용해 보고 싶은데 익숙하지 않으니 그냥 쉽게 쉽게, 예전처럼 소프트웨어개발계획서, 기본설계, 상세설계... 뭐 이런 개발 기법 다 생략하고 생각 나는데로 그때그때 개발방향 막 바꾸고 아키텍쳐 막 바꾸고.. 발코딩했습니다.  혼자서 개발하니까 이런건 좋기는 한데, 소프트웨어 개발할때 절차 다 무시해서 나중에는 팀 프로젝트하면 적응 안될것 같네요. 쓸데없는 소리가 길어졌는데, 게임은 요런식으로 동작합니다.


<게임 플레이화면>




#1 개발환경


개발툴 : Unity2D 5.6

이미지소스 : http://opengameart.org/

개발기간 : 3일


개발하면서 특이하게 어렵다거나 그런일은 없었고, 단지 재미가 없어서 몇번 갈아 엎었어요.ㅠㅠ

몇번 갈아 엎다가 그래도 별 재미 없길래 여기서 그만 ㅋㅋㅋㅋㅋㅋ



#2 스토리


토끼가 지구에 떨어졌는데, 점프해서 달나라까지 가는 내용. 그냥 어거지로 붙인거임. 점프하는 게임을 만들어보고 싶었을 뿐이에요.




#3 스크립트


게임구현에 사용한 스크립트입니다.

대충보면 아시겠지만, 별건 없어서..


타일 생성하는 GroundManager, 

토끼를 컨트롤하는 Player,

다른 잡다한 것들은 게임내에서 나오는 적들이나, 아이템 정도입니다.






#4 사용된 이미지들


늘 그렇듯, 디자이너 출신이 아니기에 게임에 들어가는 디자인들은 opengameart.org 웹 사이트에 공개된 CC0 라이센스만 가져다가 사용했습니다. CC0 라이센스는 상업적이용가능하고 수정도 자유로운 라이센스입니다. 한마디로 완전 무료인거죠.





#5 개발과정



처음에는 가로로 뛰어가는 게임을 만들었는데, 생각보다 별로였네요. 그래서 과감하게 다 지우고 다시 세로로 만들었는데...

이것도 재미없고 ㅠㅠ 이왕시작한거 끝은 봐야될것 같아서 세로화면으로 해서 끝까지 만들어봤어요.




게임시작화면입니다. 소리를 켜고 끄고 정도만 있구요, 바로 게임을 시작할 수 있도록 만들었어요.



열나게 점프해서 올라가면 되어요. 화면 아래 방향키 좌/우로 움직이면 토끼는 신기하게도 좌 우로 움직이죠? ㅋㅋㅋ 그리고 점프는 알아서 됩니다. 다만 땅을 밟아야만 되겠지요.




제때 땅을 못 밟거나, 적을 만나서 부딪치면 게임오버. 달나라까지 못갔네요.




#6 소스코드


타일 생성하는 코드입니다. 토끼가 높이 올라갈 수록 점프력도 좋아지기에, 타일의 간격도 넓어지도록 코딩했어요.


using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class GroundGenerator : MonoBehaviour {

    

    public GameObject[] grounds;

    public GameObject[] breakGrounds;

    public GameObject[] shortGrounds;

    public GameObject[] shortBreakGrounds;


    public GameObject spring;

    public GameObject jetPack;

    public GameObject bronzeCoin;

    public GameObject goldCoin;

    public GameObject silverCoin;


    private Vector2 groundAt;

    private int themeId;


    private GameObject player;

    public int stage;


    private UIManager uiManager;


    // Use this for initialization

    void Start () {

        player = GameObject.Find("Player");

        groundAt = new Vector2(0, -6.0f);

        themeId = 0;

        stage = 0;


        uiManager = GameObject.Find("Canvas").GetComponent<UIManager>();

    }


    private void FixedUpdate()

    {

        if (player.transform.position.y > groundAt.y - 5)

        {

            stage += 1;

            GenerateNextGround();

        }

    }


    private void GenerateNextGround()

    {


        int NbOfGround = 10;

        themeId = Random.Range(0, grounds.Length);

        for(int i = 0; i < NbOfGround; i++)

        {

            RespawnItems();

            RespawnGroundTile();

            

            // next ground position

            if (Random.Range(0, 2) == 0)

            {

                float mv = Random.Range(1.0f, 4.0f);

                if (mv + groundAt.x > 5.0f)

                    groundAt.x -= mv;

                else

                    groundAt.x += mv;

            }

            else

            {

                float mv = Random.Range(1.0f, 4.0f);

                if (groundAt.x - mv < -5.0f)

                    groundAt.x += mv;

                else

                    groundAt.x -= mv;

            }


            int jumpPower = uiManager.GetJumpPower();

            float range = jumpPower * 0.09f;


            groundAt.y += Random.Range(2.0f + range, 2.27f + range);

        }

    }


    void RespawnGroundTile()

    {

        int rnd = stage;

        if (stage > 6)

        {

            rnd = Random.Range(0, 6);

        }

        else

        {

            rnd = Random.Range(0, rnd);

        }


        // normal ground

        GameObject toGround;

        if (rnd >= 0 && rnd < 1)

            toGround = Instantiate(grounds[themeId], groundAt, Quaternion.identity, transform);

        else if (rnd >= 1 && rnd < 3)

            toGround = Instantiate(shortGrounds[themeId], groundAt, Quaternion.identity, transform);

        else if (rnd >= 3 && rnd < 5)

            toGround = Instantiate(breakGrounds[themeId], groundAt, Quaternion.identity, transform);

        else

            toGround = Instantiate(shortBreakGrounds[themeId], groundAt, Quaternion.identity, transform);


        // add spring by chance

        if (Random.Range(0, 20) == 1)

        {

            Vector2 at = new Vector2(Random.Range(-0.5f, 0.5f) + toGround.transform.position.x, 0.75f + toGround.transform.position.y);

            Instantiate(spring, at, Quaternion.identity, transform);

        }

            

    }


    void RespawnItems()

    {

        int rnd = Random.Range(0, 15);

        if (rnd < 5)

            BronzeCoin();

        else if (rnd >= 5 && rnd < 7)

            GoldCoin();

        else if (rnd >= 7 && rnd < 10)

            SilverCoin();

        else if (rnd == 12)

            JetPack();

    }


    void BronzeCoin()

    {

        Vector2 at = new Vector2(groundAt.x + Random.Range(-1.0f, 1.0f), groundAt.y + 1.0f);

        Instantiate(bronzeCoin, at, Quaternion.identity, transform);


        int amount = Random.Range(2, 4);

        for(int i = 0; i < amount; i++)

        {

            at.x += Random.Range(-3, 3);

            at.y += Random.Range(-2.0f, 2.0f);

            Instantiate(bronzeCoin, at, Quaternion.identity, transform);

        }

    }


    void GoldCoin()

    {

        Vector2 at = new Vector2(groundAt.x + Random.Range(-1.0f, 1.0f), groundAt.y + 1.0f);

        Instantiate(goldCoin, at, Quaternion.identity, transform);

        int amount = Random.Range(0, 4);

        for (int i = 0; i < amount; i++)

        {

            at.x += Random.Range(-3, 3);

            at.y += Random.Range(-2.0f, 2.0f);

            Instantiate(goldCoin, at, Quaternion.identity, transform);

        }

    }


    void SilverCoin()

    {

        Vector2 at = new Vector2(groundAt.x + Random.Range(-1.0f, 1.0f), groundAt.y + 1.0f);

        Instantiate(silverCoin, at, Quaternion.identity, transform);

        int amount = Random.Range(1, 4);

        for (int i = 0; i < amount; i++)

        {

            at.x += Random.Range(-3, 3);

            at.y += Random.Range(-2.0f, 2.0f);

            Instantiate(silverCoin, at, Quaternion.identity, transform);

        }

    }


    void JetPack()

    {

        Vector2 at = new Vector2(groundAt.x + Random.Range(-3.0f, 3.0f), groundAt.y + Random.Range(1.0f, 2.0f));

        Instantiate(jetPack, at, Quaternion.identity, transform);

    }

}



타일 코드입니다. 타일은 높이 올라갈 수록 움직이기도 하고 한번 밟으면 없어지기도 하게 만들었어요.


using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class Tile : MonoBehaviour {


    public Sprite damageTile;

    public int breakCounter;


    private int autoBreak;

    private GameObject player;


    private bool isMove;

    private Rigidbody2D rb2d;

    private float moveRate;

    private float nextMove;

    private float moveSpeed;

    private Vector2 originPos;


    // Use this for initialization

    void Start ()

    {

        player = GameObject.Find("Player");

        isMove = false;


        if(GameObject.Find("GroundHolder").GetComponent<GroundGenerator>().stage > 3 && Random.Range(0, 5) == 0)

        {

            rb2d = GetComponent<Rigidbody2D>();

            isMove = true;

            originPos = transform.position;

            moveSpeed = Random.Range(0, 2) == 0 ? 0.03f : -0.03f;

            moveRate = 1.0f;

            nextMove = Time.time + moveRate;

        }

}

// Update is called once per frame

void Update ()

    {

        if (!isMove) return;


   if(player.transform.position.y - transform.position.y > 10)

        {

            Destroy(gameObject);

        }


        if(Time.time > nextMove)

        {

            nextMove = Time.time + moveRate;

            moveSpeed *= -1.0f;

        }


        rb2d.MovePosition(new Vector2(transform.position.x + moveSpeed, transform.position.y));

    }

    


    private void OnCollisionExit2D(Collision2D collision)

    {

        if (breakCounter <= 0) return;


        if (collision.gameObject.tag == "Player")

        {

            breakCounter -= 1;

            if(breakCounter <= 0)

            {

                GetComponent<Animator>().SetTrigger("FadeOut");

                Destroy(gameObject, 1.0f);


                SoundManager.instance.PlayTileBreakEfx();

            }

            else if(breakCounter <= 1)

            {

                GetComponent<SpriteRenderer>().sprite = damageTile;

            }

        }

    }

}




토끼를 제어하는 코드입니다. 토끼는 모바일과 데스트탑에서 움직이는 방식이 다르기에, 입력받는 코드를 다르게 만들었어요.



using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class Player : MonoBehaviour {


    private Rigidbody2D rb2d;

    private Animator anim;


    public GameObject cloudParticle;

    public GameObject jetPack;


    public bool isForceJumping;


    public float speed;

    public float maxSpeed;

    public float jumpGroundForce;

    public float jumpSpringForce;

    public float jumpJetPackForce;

    public float maxJumpForce;


    private bool isWalk;

    private bool isJump;

    private float horizontal;


// Use this for initialization

void Start () {


        rb2d = GetComponent<Rigidbody2D>();

        anim = GetComponent<Animator>();

        isWalk = false;

        isJump = false;

        isForceJumping = false;

        jetPack.SetActive(false);


        horizontal = 0.0f;

        Invoke("JumpByGround", 2.0f);

        Invoke("RemoveGround", 2.5f);

    }

// Update is called once per frame

void Update ()

    {

        Physics2D.IgnoreLayerCollision(8, 9, rb2d.velocity.y > 0);


#if UNITY_STANDALONE || UNITY_WEBPLAYER || UNITY_EDITOR


        horizontal = Input.GetAxisRaw("Horizontal");

        rb2d.AddForce(new Vector2(horizontal, 0) * speed * Time.deltaTime);


#elif (UNITY_ANDROID || UNITY_IPHONE || UNITY_WP8)

        //horizontal = Input.acceleration.x * 1.5f;

        rb2d.AddForce(new Vector2(horizontal, 0) * speed * Time.deltaTime);

#endif


        if (Mathf.Abs(rb2d.velocity.x) > maxSpeed)

        {

            float max = rb2d.velocity.x > 0 ? maxSpeed : -maxSpeed;

            rb2d.velocity = new Vector2(max, rb2d.velocity.y);

        }


        // Animator

        if (horizontal > 0)

        {

            transform.localScale = new Vector2(1.0f, 1.0f);

            isWalk = true;

        }

        else if (horizontal < 0)

        {

            transform.localScale = new Vector2(-1.0f, 1.0f);

            isWalk = true;

        }

        else

        {

            isWalk = false;

        }


        anim.SetBool("Walk", isWalk);


        if(rb2d.velocity.y < -20)

        {

            GameObject.Find("Canvas").GetComponent<UIManager>().GameOver(false);

        }


        if(isForceJumping && rb2d.velocity.y < -0.1f)

            isForceJumping = false;

    }

    

    private void RemoveGround()

    {

        Destroy(GameObject.Find("Ground").gameObject);

    }


    public void JumpByGround()

    {

        SoundManager.instance.PlayJumpEfx();

        rb2d.velocity = new Vector2(rb2d.velocity.x, 0);

        rb2d.AddForce(new Vector2(0, jumpGroundForce));

        isJump = true;

        anim.SetBool("Jump", isJump);

        PlayCloudParticle();

    }


    public void JumpBySpring()

    {

        rb2d.velocity = new Vector2(rb2d.velocity.x, 0);

        rb2d.AddForce(new Vector2(0, jumpSpringForce));

        isJump = true;

        anim.SetBool("Jump", isJump);

        PlayCloudParticle();

    }


    public void JumpByJet()

    {

        isForceJumping = true;


        rb2d.velocity = new Vector2(rb2d.velocity.x, 0);

        rb2d.AddForce(new Vector2(0, jumpJetPackForce));

        isJump = true;

        anim.SetBool("Jump", isJump);

        PlayCloudParticle();


        jetPack.SetActive(true);

        Invoke("DeactiveJetPack", 1.0f);

    }


    public void MoveLeft()

    {

        horizontal = -1.0f;

    }

    public void MoveRight()

    {

        horizontal = 1.0f;

    }

    public void MoveStop()

    {

        horizontal = 0.0f;

    }


    private void OnCollisionEnter2D(Collision2D collision)

    {

        if (collision.gameObject.tag == "Ground")

        {

            JumpByGround();

        }

    }


    private void PlayCloudParticle()

    {

        Vector2 at = new Vector2(transform.position.x, transform.position.y - 1.2f);

        GameObject toInstance = Instantiate(cloudParticle, at, Quaternion.identity);

        toInstance.GetComponent<ParticleSystem>().Play();

        Destroy(toInstance, 1.0f);

    }


    private void DeactiveJetPack()

    {

        jetPack.SetActive(false);

    }



}




게임내 아이템중의 하나인 점프하는 스프링 코드입니다.


using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class JumpSpring : MonoBehaviour {


    public Sprite jumpActivate;

    private bool isActivated;

// Use this for initialization

void Start ()

    {

        isActivated = false;

}


    private void OnTriggerEnter2D(Collider2D collision)

    {

        if (isActivated) return;


        if(collision.tag == "Player")

        {

            collision.GetComponent<Player>().JumpBySpring();

            GetComponent<SpriteRenderer>().sprite = jumpActivate;

            isActivated = true;

            Invoke("InvokeFadeOut", 0.4f);


            SoundManager.instance.PlaySpringEfx();

        }

    }


    private void InvokeFadeOut()

    {

        GetComponent<Animator>().SetTrigger("FadeOut");

        Destroy(gameObject, 1.0f);

    }

}




마지막으로 적들 중에서 한 객체의 코드입니다. 나머지 적들도 동일한 방식으로 코딩했어요. 날라다니는 파리인데, 랜덤으로 움직이게 만들어봤어요. 토끼와 부딪히면 바로 게임오버~


using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class EnermyFlying : MonoBehaviour

{

    public string enermyName;


    private Rigidbody2D rb2d;

    private float moveRate;

    private float nextMove;

    private float moveRange;

    private bool facingRight;


    private float moveSpeed;

    private Vector2 originPos;


    private GameObject player;


    // Use this for initialization

    void Start()

    {

        rb2d = GetComponent<Rigidbody2D>();

        player = GameObject.Find("Player");


        originPos = transform.position;


        moveRate = Random.Range(4.0f, 6.0f);

        moveSpeed = Random.Range(0, 2) == 0 ? 0.01f : -0.01f;

        moveRange = Random.Range(2.0f, 4.0f);

        facingRight = Random.Range(0,2) == 0 ? true : false;

    }


    // Update is called once per frame

    void Update()

    {

        if (player.transform.position.y - transform.position.y > 10)

        {

            Destroy(gameObject);

        }


        rb2d.MovePosition(new Vector2(transform.position.x + moveSpeed, transform.position.y + moveSpeed));


        if (transform.position.x > originPos.x + moveRange && facingRight)

        {

            moveSpeed *= -1;

            facingRight = !facingRight;

        }

        else if (transform.position.x < originPos.x - moveRange && !facingRight)

        {

            moveSpeed *= -1;

            facingRight = !facingRight;

        }

    }


    private void OnTriggerEnter2D(Collider2D collision)

    {

        if (collision.tag == "Player")

        {

            if (collision.GetComponent<Player>().isForceJumping) return;

            GameObject.Find("Canvas").GetComponent<UIManager>().GameOver(true);

            Destroy(gameObject);

        }

    }

}




#7 구글 플레이 배포


자 허접하지만, 일단 만들어봤으니 배포를 해야겠지요. 구글 플레이에 배포했구요. 토끼는 점프중 앱은 아래에서 다운 받으시면 되어요^^

궁금한점이 있거나, 잘못되었거나 개선할 부분이 있다면 가차없이 뎃글 남겨 주시면 되어요.


토끼는 점프중


https://play.google.com/store/apps/details?id=com.happyryu.jumpjump&hl=ko