본문 바로가기
카테고리 없음

소스코드공개 첫번째 꼬꼬닭3D (미로찾기게임 made by Unity3D)

by 해피류 2017. 4. 20.
반응형


해피류 개발이야기 첫번째 꼬꼬닭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



반응형