해피류 개발이야기 첫번째 꼬꼬닭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주만에 개발완료한 코드라서 멍멍이 발로짠 코드입니다. 그냥 그러려니하고 참고만 해주시구요, 혹시나 잘못된점이 있거나 개선할 부분이 있다면 가차없이 덧글로 남겨주세요^^
https://play.google.com/store/apps/details?id=com.eyen.chickengo&hl=ko
최근댓글