프로그래밍) Fixers
(디펜스 게임)
1. 작업 의도
전략, RPG, 디펜스, 로그라이크 게임으로 유저가 직접 방어타워와 공격 건축물을 건설하고 강화하여 디펜스하는 게임입니다.
중세 판타지를 배경으로 '마왕군에 의해 세계가 멸망하고 희망없이 살아가던 어느날,
세계의 희망이 될 세계수 묘목을 발견하게 되고, 세계수를 성장시켜 세계가 균형을 되찾을 수 있도록 하기 위해
수호군은 4명의 전투 수리공을 그 곳으로 파견하여 그곳의 시설을 이용해 세계수가 다 성장 할 때까지 지키게 한다.'
라는 스토리를 바탕으로 하고 있습니다.
2. 작업 과정
소요 기간: 한달
Unity, Blender, Photoshop으로 제작
- 팀명: 개발새발팀
- 팀원: 프로그래밍 5명 (총 5명)
- 파트: 프로그래밍(카메라, 씬 로드, 커서), 그래픽(타이틀, 로딩씬, 3d 리소스), 사운드
개발자들로만 구성되어 있어서 개발새발팀으로 명명하였습니다. 인원이 많아서 프로그래밍에서는 카메라, 씬로드, 커서, 사운드, 그래픽쪽을 나눠서 맡게 되었습니다.
카메라 무빙
public class CameraController : MonoBehaviour
{
/* <카메라 컨트롤러 기능 구현>
* 1. 마우스휠 -> 줌인, 줌아웃
* 2. 더블 클릭 -> 캐릭터 -> 캐릭터 위치로
* 3. 더블 클릭 -> 지형 -> 클릭한 위치로 줌
* 4. 화면 테두리에 마우스 위치시 -> 카메라 이동 (조이스틱처럼)
*/
private float wheelspeed = 20.0f;
public Camera mainCamera;
public float CamMoveSpeed = 30.0f;
public GameObject Target; //화면 정중앙에 위치한 빈 게임오브젝트
Vector3 TargetPos;
//카메라 위치 고정값
public float offsetX = 0.0f;
public float offsetY = 10.0f;
public float offsetZ = -10.0f;
//줌인 줌아웃 범위 설정
public float ZoomIn = 0.5f;
public float ZoomOut = 100.0f;
void Update()
{
Zoom(); //카메라 휠 줌인, 줌아웃
}
private void FixedUpdate()
{
//더블클릭, 마우스 화면 테두리 위치시에 빈오브젝트 움직임 -> 카메라 추적
TargetPos = new Vector3(Target.transform.position.x + offsetX,
Target.transform.position.y + offsetY,
Target.transform.position.z + offsetZ);
transform.position = Vector3.Lerp(transform.position, TargetPos, Time.deltaTime * CamMoveSpeed);
}
#region 줌인,줌아웃
private void Zoom()
{
float distance = Input.GetAxis("Mouse ScrollWheel") * -1 * wheelspeed;
if (distance != 0)
mainCamera.fieldOfView += distance;
if (mainCamera.fieldOfView < ZoomIn)
mainCamera.fieldOfView = ZoomIn;
if (mainCamera.fieldOfView > ZoomOut)
mainCamera.fieldOfView = ZoomOut;
}
#endregion 줌인,줌아웃
}
카메라 무빙2
public class MousePick : MonoBehaviour
{
static MousePick _instance;
public static MousePick Instance { get { return _instance; } }
public float moveSpeed = 20.0f;
public float DoubleClickSecond = 0.25f;
private bool IsOneClick = false;
private double Timer = 0;
public bool isMove;
public Vector3 destination;
public bool IsDoubleClick = false;
public Camera mainCamera;
public float view = 60;
void Start()
{
Init();
}
void Update()
{
DoubleClicked();
Move();
Debug.Log(destination);
Vector2 MousePosition = Input.mousePosition;
if ((MousePosition.x <= 0) || (MousePosition.y <= 0)
|| (MousePosition.x >= Screen.width) || (MousePosition.y >= Screen.height))
{
MoveScreen(MousePosition);
}
}
#region 더블클릭 인식
public void DoubleClicked()
{
if (IsOneClick && ((Time.time - Timer) > DoubleClickSecond))
{
IsOneClick = false;
IsDoubleClick = false;
}
if (Input.GetMouseButtonDown(0))
{
if (!IsOneClick)
{
Timer = Time.time;
IsOneClick = true;
IsDoubleClick = false;
}
else if (IsOneClick && ((Time.time - Timer) < DoubleClickSecond))
{
Debug.Log("Double Click");
//if (ResourceManager.Instance != null) ResourceManager.Instance.Destination(destination);
IsDoubleClick = true;
IsOneClick = false;
RaycastHit hit;
if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hit))
{
destination = hit.point;
isMove = true;
}
}
}
}
#endregion 더블클릭 인식
#region 빈오브젝트 이동
public void Move()
{
if (isMove)
{
bool isAlived = Vector3.Distance(destination, transform.position) <= 0.1f;
if (isAlived)
{
isMove = false;
}
else
{
//Vector3 direction = destination - transform.position;
//transform.forward = direction;
//transform.position += direction.normalized * moveSpeed * Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, destination, Time.deltaTime * moveSpeed);
mainCamera.fieldOfView = view;
}
}
}
#endregion 빈오브젝트 이동
#region 화면모서리 이동
private void MoveScreen(Vector2 MousePosition)
{
if (MousePosition.x <= 0)
transform.position += new Vector3(-1, 0, 0) * Time.deltaTime * moveSpeed;
else if (MousePosition.y <= 0)
transform.position += new Vector3(0, 0, -1) * Time.deltaTime * moveSpeed;
else if (MousePosition.x >= Screen.width)
transform.position += new Vector3(1, 0, 0) * Time.deltaTime * moveSpeed;
else if (MousePosition.y >= Screen.height)
transform.position += new Vector3(0, 0, 1) * Time.deltaTime * moveSpeed;
else
return;
}
#endregion 화면 모서리 이동
static void Init()
{
if (_instance == null)
{
GameObject go = GameObject.Find("Target");
if (go == null)
{
go = new GameObject { name = "Target" };
go.AddComponent<MousePick>();
}
DontDestroyOnLoad(go);
_instance = go.GetComponent<MousePick>();
}
}
}
마우스가 화면 상하좌우에 위치시에 스타크래프트처럼 화면이 이동하도록 하는 기능과 줌인 줌아웃, 더블 클릭시에 줌이 해제되고 화면의 중앙이 좌표로 이동하도록 하였습니다.
목적지 표시
public class ResourceManager : MonoBehaviour
{
static ResourceManager _instance;
public static ResourceManager Instance { get { return _instance; } }
private GameObject obj1;
private GameObject obj2;
private bool isdc = false;
private bool next = false;
private float frameTimer = 0.2f;
private float currentTimer;
private void Start()
{
Init();
obj1 = Resources.Load<GameObject>("Prefabs/click1");
obj2 = Resources.Load<GameObject>("Prefabs/click2");
}
private void Update()
{
GameObject obj1Clone = GameObject.Find("click1(Clone)");
GameObject obj2Clone = GameObject.Find("click2(Clone)");
currentTimer += Time.deltaTime;
if (MousePick.Instance.IsDoubleClick)
{
Vector3 a = MousePick.Instance.destination;
a.y = 1;
obj1.transform.position = a;
obj2.transform.position = a;
isdc = true;
MousePick.Instance.IsDoubleClick = false;
}
if (isdc && next == false)
{
Instantiate(obj1);
isdc = false;
currentTimer = 0;
}
if (obj1Clone != null && currentTimer >= frameTimer)
{
Destroy(obj1Clone);
next = true;
}
if (next && isdc == false)
{
Instantiate(obj2);
next = false;
currentTimer = 0;
}
if (obj2Clone != null && currentTimer >= frameTimer)
{
Destroy(obj2Clone);
currentTimer = 0;
//if (MousePick.Instance.destination == MousePick.Instance.transform.position) isdc = false;
if (MousePick.Instance.isMove) isdc = true;
else isdc = false;
}
}
}
더블 클릭시에 바닥에 목적지를 표시하는 이미지가 뜨도록 하였습니다.
씬 로드
public class LoadingSceneManager : MonoBehaviour
{
public static string nextScene;
[SerializeField] Image progressBar;
private void Start()
{
StartCoroutine(LoadScene());
}
public static void LoadScene(string sceneName)
{
nextScene = sceneName;
SceneManager.LoadScene("LoadingScene");
}
IEnumerator LoadScene()
{
yield return null;
AsyncOperation op = SceneManager.LoadSceneAsync(nextScene);
op.allowSceneActivation = false;
float timer = 0.0f;
while (!op.isDone)
{
yield return null;
timer += Time.deltaTime;
if (op.progress < 0.9f)
{
progressBar.fillAmount = Mathf.Lerp(progressBar.fillAmount, op.progress, timer);
if (progressBar.fillAmount >= op.progress)
{
timer = 0f;
}
}
else
{
progressBar.fillAmount = Mathf.Lerp(progressBar.fillAmount, 1f, timer);
if (progressBar.fillAmount == 1.0f)
{
op.allowSceneActivation = true;
yield break;
}
}
}
}
}
public class Title : MonoBehaviour
{
AudioSource audioSource;
Collider2D collision;
public GameObject Button;
public void OnTriggerExit2D(Collider2D collision)
{
SceneManager.LoadScene("MainScene"); //클릭했을때 로드할 씬 이름
}
public void Press_Play()
{
audioSource = Button.gameObject.GetComponent<AudioSource>();
this.audioSource.Play();
OnTriggerExit2D(collision);
}
}
타이틀 화면에서 스타트 버튼을 클릭시에 로딩화면을 거쳐서 메인 화면으로 넘어갈 수 있도록 코딩하였습니다.
마우스커서
public class CursorController : MonoBehaviour
{
static CursorController _instance;
public static CursorController Instance { get { return _instance; } }
public List<CursorAnimation> cursorAnimationList;
public CursorAnimation cursorAnimation;
private int currentFrame;
private float frameTimer;
private int frameCount;
public enum CursorType
{
Arrow,
Grab
}
public void Start()
{
SetActiveCursorAnimation(cursorAnimationList[0]);
Init();
}
private void Update()
{
frameTimer -= Time.deltaTime;
if (frameTimer <= 0f)
{
frameTimer += cursorAnimation.frameRate;
currentFrame = (currentFrame + 1) % frameCount;
Cursor.SetCursor(cursorAnimation.textureArray[currentFrame],cursorAnimation.offset, CursorMode.Auto);
}
//if (gameObject.CompareTag("Wall")) SetActiveCursorAnimation(cursorAnimationList[1]);
}
public void SetActiveCursorAnimation(CursorAnimation cursorAnimation)
{
this.cursorAnimation = cursorAnimation;
currentFrame = 0;
frameTimer = cursorAnimation.frameRate;
frameCount = cursorAnimation.textureArray.Length;
}
[System.Serializable]
public class CursorAnimation
{
public CursorType cursorType;
public Texture2D[] textureArray;
public float frameRate;
public Vector2 offset;
}
static void Init()
{
if (_instance == null)
{
GameObject go = GameObject.Find("@CursorManager");
if (go == null)
{
go = new GameObject { name = "@CursorManager" };
go.AddComponent<CursorController>();
}
DontDestroyOnLoad(go);
_instance = go.GetComponent<CursorController>();
}
}
}
public class CursorObject : MonoBehaviour
{
//[SerializeField] private CursorController.CursorType cursorType;
//마우스가 오브젝트 안으로 들어가면 커서가 손 모양으로 바뀜
private void OnMouseEnter()
{
CursorController.Instance.SetActiveCursorAnimation(CursorController.Instance.cursorAnimationList[1]);
Debug.Log("성벽 터치");
}
private void OnMouseExit()
{
CursorController.Instance.SetActiveCursorAnimation(CursorController.Instance.cursorAnimationList[0]);
}
}
마우스 커서를 바꿔주었고 성벽을 터치하면 성벽 터치 로그를 표시하고 커서가 변경되도록 하였습니다.
3. 결과물
게임을 제작해본적 없는 열정 가득한 프로그래머 5명이 모였으나 용두사미로 끝나서 아쉬웠습니다. 깃허브를 사용하지 않고 각자 맡은 부분을 코딩하고 취합하려다 보니 마무리가 잘 되지 않았습니다. 아카데미가 끝나고 프로젝트를 제대로 마무리하기로 하였으나 게임 업계에 취업이 되었거나, 취업을 비희망하는 팀원이 발생함에 따라 마무리가 잘 되지 않았습니다. 매우 아쉬웠습니다. 다음에 비슷한 상황이 생기게 된다면 꼭 깃허브를 이용해서 작업하고, 폴더 정리나 리소스 관리에 관한 규칙을 정해서 좋은 결과물이 나올 수 있도록 하고 싶습니다.