해피류 개발이야기 첫번째 꼬꼬닭3D



유니티를 혼자서 독학을 시작한지 일주일이 지나고 이주째되는날부터 치킨고라는 간단한 3D 앱을 개발해서 구글플레이에 배포했답니다. 그 과정을 이제서야 포스팅해보아요. C#을 모르고 시작했었고, 유니티는 어떻게 하는지도 몰랐으니 코딩은 물론 발로 했겠지요. 그냥 참고만 해 주세요. 이글을 보는 사람이 있을지도 모르겠네요. 먼훗날 우리 류망아지가 커서 볼 수도 있으니 일단 남겨봅니다.


<게임플레이화면>




#1 개발환경


개발툴 : Unity3D 5.5

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

개발기간 : 1주일


닭에게 텍스쳐입히는 방법 몰라서 삽질하고, UI구성을 어떻게 해야될지 몰라서 삽질하고 뭐 그랬어요 ㅋㅋ



#2 스토리


스토리라인, 뭐 이딴거 없어요ㅋㅋㅋ 

그냥 닭이 미로에 있고 알을 찾으면 미로를 탈출하는 허접한 내용이에요.



#3 스크립트


사용된 스크립트입니다. 

프로그램 전체에서 모두 사용할 수 있게 싱글턴으로 구현했는 SoundManager,  GameManager 

나머지것들은 그냥 객체를 컨트롤하기 위해서 대충 필요에 의해서 만들었어요



#4 닭 움직이는 컨트롤러


유니티에서 CNControls 라고 검색하면 패키지가 하나 나오는데 그거 사용했어요. 생각보다 편하더라구요.




#5 사용된 이미지들


모든 이미지들은 CC0 라이센스로 배포된 것들만 사용했어요.

무료로 배포해주셔서 감사합니다.





#6 개발과정


먼저 꼬꼬닭 3D 객체를 무료배포사이트에서 찾았습니다. 일단 닭을 적당하게 만들고요. ㅠㅠ

처음 유니티를 접하는 것이라서 Collider를 붙이고 Rigidbody도 넣어주고 어떻게 움직이는지 몰라 인터넷으로 검색도 하고 삽질 많이 했어요. 닭 원하는데로 움직이는데만 하루 걸린듯 ㅋㅋㅋ



두번째로 미로도 생성해주어요. 미로를 생성하는 소스코드출처 : 바로가기




그냥 미로만 달리게 했더니만 먼가 재미도 없고 그래서 아이템을 추가했어요. 동전이랑 수박. 동전 모아서 전체보기, 게임시간구매를 할 수있도록했구요, 수박을 먹으면 FEVER 타임이 추가되어서 꼬꼬닭이 대빠 빠른속도로 달릴 수 있도록 구현해 보았어요.





#7 소스코드 공개


닭을따라 움직이는 코드 공개합니다.


isCameraViewMode : 닭은 고정시키고 현재 닭의 위치를 중심으로 카메라를 좌우로 돌려보고 싶을때 사용하는 모드입니다.


using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using CnControls;


public class CamaraController : MonoBehaviour {


    public GameObject chicken;

    public float smoothing = 5f;


    private Vector3 offset;

    private Vector3 movement;

    private Quaternion rotation;

    private Camera viewCamera;


    public float smooth = 0.8f;

    public float tiltAngle = 120f;


    private void Awake()

    {

        viewCamera = GetComponent<Camera>();


        offset = transform.position - chicken.transform.position;

        rotation = transform.rotation;

    }


    private void FixedUpdate()

    {

        if(!GameManager.instance.isCameraViewMode)

        {

            Vector3 targetCamPos = offset + chicken.transform.position;

            transform.position = Vector3.Lerp(transform.position, targetCamPos, smoothing * Time.deltaTime);

        }

        else

        {

            float tiltAroundZ = CnInputManager.GetAxis("Horizontal") * tiltAngle;

            //float tiltAroundX = CnInputManager.GetAxis("Vertical") * tiltAngle;

            Quaternion target = Quaternion.Euler(40.0f, tiltAroundZ, 0f);

            transform.rotation = Quaternion.Slerp(transform.rotation, target, Time.deltaTime * smooth);

            viewCamera.fieldOfView = 70f;

        }

    }


    public void ActivateCameraViewMode()

    {

        if(GameManager.instance.telescope > 0)

        {

            GameManager.instance.telescope -= 1;

            GameManager.instance.isCameraViewMode = true;

            GameManager.instance.hp -= 10;

        }

        else

        {

            ChickenController chicken = GameObject.Find("Chicken").GetComponent<ChickenController>();

            if(chicken)

            {

                chicken.showCommentMessage("BUY TELESCOPE FIRST", Color.white);

            }

        }

    }


    public void DeactivateCameraViewMode()

    {

        GameManager.instance.isCameraViewMode = false;

        transform.rotation = rotation;

        transform.position = offset + chicken.transform.position;

        viewCamera.fieldOfView = 30f;

    }

}




게임내 동전이 계속 돌아가게 만드는 코드입니다. 별거 없어요, 그냥 Rotate 함수만 사용하니 끝! 대박 간단하네요 ㅎㅎㅎ


using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class CoinRotator : MonoBehaviour {


    void Update ()

    {

        transform.Rotate(new Vector3(0, 0, 50) * Time.deltaTime);

    }

}


사운드 매니져도 간단! 그냥 유니티 홈페이지에서 튜토리얼에 있는 코드 가져다 와서 썼어요.


using UnityEngine;

using System.Collections;


public class SoundManager : MonoBehaviour

{

    public AudioSource efxSource;                   //Drag a reference to the audio source which will play the sound effects.

    public AudioSource musicSource;                 //Drag a reference to the audio source which will play the music.

    public AudioSource walkSource;                 //Drag a reference to the audio source which will play the music.

    public static SoundManager instance = null;     //Allows other scripts to call functions from SoundManager.             

    public float lowPitchRange = .95f;              //The lowest a sound effect will be randomly pitched.

    public float highPitchRange = 1.05f;            //The highest a sound effect will be randomly pitched.



    void Awake()

    {

        //Check if there is already an instance of SoundManager

        if (instance == null)

            //if not, set it to this.

            instance = this;

        //If instance already exists:

        else if (instance != this)

            //Destroy this, this enforces our singleton pattern so there can only be one instance of SoundManager.

            Destroy(gameObject);


        //Set SoundManager to DontDestroyOnLoad so that it won't be destroyed when reloading our scene.

        DontDestroyOnLoad(gameObject);


        musicSource.volume = 0.2f;

    }


    public void PlayWalk(AudioClip clip)

    {

        //Set the clip of our efxSource audio source to the clip passed in as a parameter.

        walkSource.clip = clip;


        //Play the clip.

        walkSource.Play();

    }


    //Used to play single sound clips.

    public void PlaySingle(AudioClip clip)

    {

        //Set the clip of our efxSource audio source to the clip passed in as a parameter.

        efxSource.clip = clip;


        //Play the clip.

        efxSource.Play();

    }



    //RandomizeSfx chooses randomly between various audio clips and slightly changes their pitch.

    public void RandomizeSfx(params AudioClip[] clips)

    {

        //Generate a random number between 0 and the length of our array of clips passed in.

        int randomIndex = Random.Range(0, clips.Length);


        //Choose a random pitch to play back our clip at between our high and low pitch ranges.

        float randomPitch = Random.Range(lowPitchRange, highPitchRange);


        //Set the pitch of the audio source to the randomly chosen pitch.

        efxSource.pitch = randomPitch;


        //Set the clip to the clip at our randomly chosen index.

        efxSource.clip = clips[randomIndex];


        //Play the clip.

        efxSource.Play();

    }



}


중요한 맵 생성코드. 하나 하나 설명할려니... (코어부분 소스코드출처 : 바로가기)

그냥 붙여넣기해놨어요. 혹시나 질문있으시면 밑에 덧글 남겨주세요^^


using UnityEngine;

using System;

using System.Collections;

using System.Collections.Generic;

using Random = UnityEngine.Random;


public class MazeGenerator : MonoBehaviour

{

    public GameObject Egg;

    public GameObject[] Blocks;

    public GameObject[] Planes;

    public GameObject Melon;

    public GameObject Coin;


    private Transform MazeHolder;

    private int[,] Maze;

    

    private Stack<Vector2> tiles = new Stack<Vector2>();

    private List<Vector2> offsets = new List<Vector2> { new Vector2(0, 1), new Vector2(0, -1), new Vector2(1, 0), new Vector2(-1, 0) };

    private Vector2 currentTile;

    private int width = 11, height = 11;


    public Vector2 CurrentTile

    {

        get { return currentTile; }

        private set

        {

            if (value.x < 1 || value.x >= this.width - 1 || value.y < 1 || value.y >= this.height - 1)

            {

                throw new ArgumentException("Width and Height must be greater than 2 to make a maze");

            }

            currentTile = value;

        }

    }


    public void SetupScene(int Level)

    {

        tiles.Clear();

        width = 7 + Level * 2;

        height = 7 + Level * 2;


        MakeBlocks();

        InstantiateBlocks();

        InstantiateEgg();

        InstantiatePlanes();


        InstantiateFood();

        InstantiateCoin();

    }


    void MakeBlocks()

    {

        Maze = new int[width, height];

        for (int x = 0; x < width; x++)

        {

            for (int y = 0; y < height; y++)

                Maze[x, y] = 1;

        }


        CurrentTile = Vector2.one;

        tiles.Push(CurrentTile);


        Maze = CreateMaze();

    }


    public int[,] CreateMaze()

    {

        List<Vector2> neighbors;

        while (tiles.Count > 0)

        {

            Maze[(int)CurrentTile.x, (int)CurrentTile.y] = 0;

            neighbors = GetValidNeighbors(CurrentTile);

            if (neighbors.Count > 0)

            {

                tiles.Push(CurrentTile);

                CurrentTile = neighbors[Random.Range(0, neighbors.Count)];

            }

            else

            {

                CurrentTile = tiles.Pop();

            }

        }

        print("Maze Generated ...");

        return Maze;

    }


    private List<Vector2> GetValidNeighbors(Vector2 centerTile)

    {

        List<Vector2> validNeighbors = new List<Vector2>();

        foreach (var offset in offsets)

        {

            Vector2 toCheck = new Vector2(centerTile.x + offset.x, centerTile.y + offset.y);

            if (toCheck.x % 2 == 1 || toCheck.y % 2 == 1)

            {

                if (Maze[(int)toCheck.x, (int)toCheck.y] == 1 && HasThreeWallsIntact(toCheck))

                {

                    validNeighbors.Add(toCheck);

                }

            }

        }

        return validNeighbors;

    }


    private bool HasThreeWallsIntact(Vector2 Vector2ToCheck)

    {

        int intactWallCounter = 0;

        foreach (var offset in offsets)

        {

            Vector2 neighborToCheck = new Vector2(Vector2ToCheck.x + offset.x, Vector2ToCheck.y + offset.y);

            if (IsInside(neighborToCheck) && Maze[(int)neighborToCheck.x, (int)neighborToCheck.y] == 1)

            {

                intactWallCounter++;

            }

        }

        return intactWallCounter == 3;

    }


    // ================================================

    private bool IsInside(Vector2 p)

    {

        return p.x >= 0 && p.y >= 0 && p.x < width && p.y < height;

    }


    void InstantiateBlocks()

    {

        MazeHolder = new GameObject("MazeBoard").transform;


        int blockId = Random.Range(0, Blocks.Length);

        

        for (int i = 0; i <= Maze.GetUpperBound(0); i++)

        {

            for (int j = 0; j <= Maze.GetUpperBound(1); j++)

            {

                if (Maze[i, j] == 1)

                {

                    GameObject toBlock = Blocks[blockId];

                    Vector3 position = new Vector3(i, toBlock.transform.position.y, j);

                    GameObject toInstance = Instantiate(toBlock, position, Quaternion.identity) as GameObject;

                    toInstance.transform.SetParent(MazeHolder);

                }

            }

        }

    }


    private void InstantiateEgg()

    {

        int RandomPosition = Random.Range(0, 2);

        bool isDone = false;


        if(RandomPosition == 0)

        {

            for (int i = Maze.GetUpperBound(0); i >= 0; i--)

            {

                for (int j = Maze.GetUpperBound(1); j >= 0; j--)

                {

                    Vector2 toCheck = new Vector2(i, j);

                    if (Maze[i, j] == 0 && HasThreeWallsIntact(toCheck))

                    {

                        Vector3 position = new Vector3(i, Egg.transform.position.y, j);

                        GameObject toInstance = Instantiate(Egg, position, Quaternion.identity) as GameObject;

                        toInstance.transform.SetParent(MazeHolder);

                        Maze[i, j] = 1;

                        isDone = true;

                        break;

                    }

                }

                if (isDone) break;

            }

        }

        else

        {

            for (int i = Maze.GetUpperBound(0); i >= 0; i--)

            {

                for (int j = 0; j <= Maze.GetUpperBound(1); j++)

                {

                    Vector2 toCheck = new Vector2(i, j);

                    if (Maze[i, j] == 0 && HasThreeWallsIntact(toCheck))

                    {

                        Vector3 position = new Vector3(i, Egg.transform.position.y, j);

                        GameObject toInstance = Instantiate(Egg, position, Quaternion.identity) as GameObject;

                        toInstance.transform.SetParent(MazeHolder);

                        Maze[i, j] = 1;

                        isDone = true;

                        break;

                    }

                }

                if (isDone) break;

            }

        }

    }



    private void InstantiatePlanes()

    {

        int NbOfPlanes = width / 6 + 1;

        int PlaneId = Random.Range(0, Planes.Length);

        GameObject plane = Planes[PlaneId];

        Debug.Log(PlaneId);

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

        {

            for(int j = 0; j < NbOfPlanes; j++)

            {

                Vector3 position = new Vector3(i * 10, 0, j * 10);

                GameObject toInstance = Instantiate(plane, position, Quaternion.identity) as GameObject;

                toInstance.transform.SetParent(MazeHolder);

            }

        }

    }


    private void InstantiateFood()

    {

        int NbOfFood = GameManager.instance.level + Random.Range(0, GameManager.instance.level);

        for(int melon = 0; melon < NbOfFood; melon++)

        {

            bool isDone = false;

            while(!isDone)

            {

                int i = Random.Range(0, width);

                int j = Random.Range(0, height);


                if(Maze[i,j] == 0)

                {

                    Vector3 position = new Vector3(i, 0, j);

                    GameObject toInstance = Instantiate(Melon, position, Quaternion.identity) as GameObject;

                    toInstance.transform.SetParent(MazeHolder);

                    Maze[i, j] = 1;


                    isDone = true;

                }

            }

        }

    }


    private void InstantiateCoin()

    {

        int MaxIteration = 0;

        int NbOfCoins = GameManager.instance.level * 10;

        for (int coins = 0; coins < NbOfCoins; coins++)

        {

            bool isDone = false;

            while (!isDone)

            {

                MaxIteration += 1;

                int i = Random.Range(0, width);

                int j = Random.Range(0, height);


                if (Maze[i, j] == 0)

                {

                    Vector3 position = new Vector3(i, 0.3f, j);

                    Quaternion target = Quaternion.Euler(90f, Coin.transform.rotation.x, Coin.transform.rotation.x);

                    GameObject toInstance = Instantiate(Coin, position, target) as GameObject;

                    toInstance.transform.SetParent(MazeHolder);

                    Maze[i, j] = 1;


                    isDone = true;

                }

                if (MaxIteration > 100) isDone = true;

            }

        }

    }


}





그리고 마지막으로 게임매니져. 닭이 미로에서 탈출하면 새로운 미로를 생성해줍니다. 다만, 스테이지가 올라갈 수록 미로의 크기는 점점 커지게 설계했어요.


using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.UI;


public class GameManager : MonoBehaviour {


    public static GameManager instance = null;    

    private MazeGenerator maze;                   

    public int level = 1;

    public float hp = 5f;

    public float fever = 0f;

    public int coin = 0;

    public int telescope = 3;

    public bool isCameraViewMode = false;

    public bool isInShop = false;

    public bool isReady = true;

    public bool isDead = false;


    private void Awake()

    {

        if (instance == null)

            instance = this;

        else if (instance != this)

            Destroy(gameObject);


        int clearedStage = PlayerPrefs.GetInt("Stage", 0);

        if (clearedStage != 0)

            level = clearedStage;


        DontDestroyOnLoad(gameObject);

        maze = GetComponent<MazeGenerator>();

        InitGame();

    }


    private void InitGame()

    {

        maze.SetupScene(level);

    }


    private void OnLevelWasLoaded(int index)

    {

        int clearedStage = PlayerPrefs.GetInt("Stage", 0);

        level = clearedStage + 1;

        PlayerPrefs.SetInt("Stage", level);

        InitGame();

    }


    private void Update()

    {

        if (Application.platform == RuntimePlatform.Android)

        {

            if (Input.GetKey(KeyCode.Escape))

            {

                Application.Quit();

            }

        }

    }


}




#8 구글 플레이 배포


자~ 이상 소스코드 대 공개였습니다.

앞에서도 언급했지만, 맨땅에서 유니티랑 C# 학습하면서 2주만에 개발완료한 코드라서 멍멍이 발로짠 코드입니다. 그냥 그러려니하고 참고만 해주시구요, 혹시나 잘못된점이 있거나 개선할 부분이 있다면 가차없이 덧글로 남겨주세요^^



꼬꼬닭3D 다운받기


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