publicclassPlayerMove : MonoBehaviour
{
publicbool isPoint = false;
publicint point = 0;
publicfloat Speed;
privateint directionIs = 0;
Rigidbody2D rigid;
// Start is called before the first frame updatevoidAwake()
{
}
// Update is called once per framevoidUpdate()
{
Rotation();
Rotate();
}
voidFixedUpdate()
{
Move();
}
privatevoidRotation()
{
//키입력 시에 방향전환if (Input.GetKeyDown(KeyCode.D))
{
if (directionIs == 0)
directionIs = 3;
else
directionIs = 0;
}
if (Input.GetKeyDown(KeyCode.A))
{
if (directionIs == 1)
directionIs = 2;
else
directionIs = 1;
}
}
privatevoidMove()
{
if (directionIs==0)
transform.position = new Vector2(transform.position.x, transform.position.y + Speed);
elseif (directionIs==1)
transform.position = new Vector2(transform.position.x + Speed, transform.position.y);
elseif (directionIs==2)
transform.position = new Vector2(transform.position.x - Speed, transform.position.y);
elseif (directionIs==3)
transform.position = new Vector2(transform.position.x, transform.position.y - Speed);
}
privatevoidOnCollisionEnter2D(Collision2D other)
{
if (other.gameObject.tag == "Wall")
{
Debug.Log("GameOver");
}
}
privatevoidOnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.tag == "PointBox")
{
isPoint = true;
}
}
privatevoidRotate() //부드럽게 만들기
{
if (Input.GetMouseButtonDown(0))
{
for(int i=1; i<91; i++)
{
transform.Rotate(0, 0, i);
}
}
}
privatevoidOnTriggerExit2D(Collider2D other)
{
if (other.gameObject.name == "PointBox")
{
isPoint = false;
}
}
}
pointbox c#스크립트
publicclassPoint : MonoBehaviour
{
public GameObject PlayerMove;
privatebool istouch = false;
voidStart()
{
}
// Update is called once per framevoidUpdate()
{
Delete();
}
privatevoidDelete()
{
if ((Input.GetKeyDown(KeyCode.Space)||Input.GetKeyDown(KeyCode.A)||Input.GetKeyDown(KeyCode.D))&&PlayerMove.GetComponent<PlayerMove>().isPoint&&istouch)
{
PlayerMove.GetComponent<PlayerMove>().point++;
print(PlayerMove.GetComponent<PlayerMove>().point);
Destroy(this.gameObject);
}
}
privatevoidOnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.name == "Player")
{
istouch = true;
}
}
privatevoidOnTriggerExit2D(Collider2D other)
{
if (other.gameObject.tag == "Player")
{
istouch = false;
}
}
}
'A' 키를 눌러야 할 때에는 빨강색, 'D'는 파랑색으로 지정했다.
그리고 지난번 방향 전환에서 좀 더 추가해서 'A' 키는 좌우를, 'D'키는 위아래를 움직일 수 있도록 했다.
첫번째로 만들 리듬게임의 음악은 '베토벤 바이러스'로 정했다. 그 후 turn 혹은 space바를 눌러야하는 지점에 pointbox를 깔아주었다.
Player가 pointbox와 닿았을 때 키를 누르면 pointbox가 사라지도록 하기 위해 OnTriggerEnter2D 함수 안에서 Input을 사용해봤지만 생각대로 작동하지 않았다. 그 이유는 OnTriggerEnter는 1 프레임에만 사용되기 때문이였다. 동일한 프레임(1 프레임에만 해당)에서 키를 눌러야 작동하는 것이었다.
그래서 bool 변수를 하나 선언해서 trigger 안에 있을 때와 없을 때 각각 OntriggerEnter와 OntriggerExit를 이용해 bool 값을 지정해주었고, pointbox에 적용할 스크립트를 생성해 player에 적용한 스크립트에서 bool 변수를 가져와 pointbox에 닿았을 때 키를 누르면 pointbox가 사라지도록 만들었다.
일단 이정도까지 하고 난 뒤 게임을 실행했을 때 하나의 pointbox를 먹으면 모든 pointbox가 사라지는 문제가 발생했다. 그래서 pointbox의 c# 스크립트에서 onTriggerEnter과 onTriggerExit를 이용해 문제를 해결했다.
나는 공부를 하거나 코딩, 인터넷 서칭을 할 때 음악을 거의 항상 듣는다. 노래를 들으며 수학 문제를 풀면 어느 순간 노래의 박자나 리듬을 타며 신나게 문제를 풀고 있는 나를 종종 발견할 수 있었다.
노래의 리듬을 타며 할 일을 하는 것이 나에게는 힐링으로 다가왔고, 내가 좋아하는 노래를 내가 주로 탔던 리듬으로 리듬 게임을 만들고 싶다는 생각으로 이어졌다.
내가 주로 리듬을 타며 들었던 노래들은 팝송이었는데, 유명한 팝송을 이용해 게임을 만들 경우에는 저작권 침해 문제가 발생할 것이라고 생각되어 팝송으로 리듬게임을 만드는 것은 포기했다.
그래서 '베토벤 바이러스'와 같이 음악의 저작권자가 사망한지 70년이 지난 음악들이나 무료로 풀리는 음악들로만 만들고자 한다.
많은 리듬게임이 있지만 그중 다수의 게임들은 외우지 않으면 눈감고 클리어를 할 수 없었다. 어릴 때 부터 꽤나 많은 리듬게임들을 해왔지만 나는 보고 누르는 것이 느려 항상 리듬게임을 즐겁게 플레이 하지 못했다. 귀보다는 눈이 더 열일 하는 느낌이 강했다. 그래서 나는 귀가 조금 더 열일 하는 나를 위한 리듬게임을 만들고자 한다. (얼불춤처럼)
게임 형태(컨셉)에 관해 일주일동안 고민해 많고 좋은 컨셉들을 생각해냈지만 아직 아는 것이 거의 없는 점을 고려해 실현 가능한 형태로 골랐다.
간단하게 소개하자면 player가 박자에 맞춰 방향 전환을 하며 지그재그로 된 통로에 부딪히지 않는 2D 게임이다.
방향 키는 'A'와 'D' 두 개를 이용할 예정이고, pc 버전으로 먼저 만들고 모바일 버전도 만들어 볼 예정이다.
현재까지 작성한 코드는 이러하다.
publicfloat Speed;
publicint directionIs = 0;
// Start is called before the first frame updatevoidAwake()
{
}
// Update is called once per framevoidUpdate()
{
//키입력 시에 방향전환if (Input.GetKeyDown(KeyCode.D))
{
directionIs = 0;
}
if (Input.GetKeyDown(KeyCode.A))
{
directionIs = 1;
}
}
voidFixedUpdate()
{
//Moveif (directionIs==0)
transform.position = new Vector2(transform.position.x, transform.position.y + Speed);
elseif (directionIs==1)
transform.position = new Vector2(transform.position.x + Speed, transform.position.y);
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
publicclassPlayerMove : MonoBehaviour
{
publicfloat maxSpeed;
Rigidbody2D rigid;
SpriteRenderer spriteRenderer;
Animator anim;
// Start is called before the first frame updatevoidAwake() {
rigid = GetComponent<Rigidbody2D>();
spriteRenderer = GetComponent<SpriteRenderer>();
anim = GetComponent<Animator>();
}
// Update is called once per framevoidUpdate()
{
//Stop Speed if(Input.GetButtonUp("Horizontal")){
rigid.velocity = new Vector2(rigid.velocity.normalized.x*0.2f, rigid.velocity.y);
}
//Direction Spriteif(Input.GetButtonDown("Horizontal")){
spriteRenderer.flipX = Input.GetAxisRaw("Horizontal") == -1;
}
//Walk <-> Breathif(Mathf.Abs(rigid.velocity.x)<0.3f)
anim.SetBool("isWalking",false);
else
anim.SetBool("isWalking",true);
}
voidFixedUpdate()
{
//Move By Controlfloat h=Input.GetAxisRaw("Horizontal");
rigid.AddForce(Vector2.right*h,ForceMode2D.Impulse);
if(rigid.velocity.x > maxSpeed)
rigid.velocity = new Vector2(maxSpeed, rigid.velocity.y); // Right Max Speedelseif(rigid.velocity.x < maxSpeed*(-1))
rigid.velocity = new Vector2(maxSpeed*(-1), rigid.velocity.y); // Left Max Speed
}
}
단발적인 키 입력은 Updated에서, 계속적인 키 입력은 FixedUpdate에서 해준다.
Unity에 있는 기능들을 C# 스크립트로 불러와 적절하게 사용할 수 있다. 이때, 초기화 해주는 것을 잊지 말자.
public으로 maxSpeed 변수를 생성해주면 유니티에서 값을 넣어주는 칸이 생성되어 편하다.
normalized - 벡터의 크기를 1로 만든 상태이다. (단위벡터)
선형대수학을 배울 때 단위벡터는 방향을 나타내기 위해 많이 쓰였는데 유니티에서도 마찬가지다.
python에서의 절댓값 함수 abs와 같은 기능을 하는 것은 Mathf.Abs이다.
Mathf는 수학 관련 함수를 제공하는 class이다.
Friction
player가 원활하게 움직일 수 있도록 Physics Material 2D를 생성해 마찰력을 적당히 지정해준다.
참고로 Friction이 마찰력이며, 0으로 지정해줄 경우 마찰력이 없는 빙판과 같다.
오르막길을 잘 오를 수 있도록 하기 위해서 마찰력을 0으로 설정해주었다.
만든 재질을 지형의 Collider 2D - Material에 끌어다 넣어주면 잘 적용된다.
Rigid Body에서 공기저항
Linear Drag - 공기저항, 이동 시 속도를 느리게 해준다. 보통 1~2로 설정해준다.
Animator 설정
움직이는 상태, 멈추고 있는 상태 등 상황에 따라 나타낼 player의 animation은 다르므로 코드와 함께 animator를 수정해줘야 한다. breath가 default state인 상태에서 player가 움직일 경우에는 walk 동작을 해야하므로 마우스 우클릭을 통해 breath에서 walk로의 화살표를 만들어 줘야한다. 움직이다가 멈추는 경우도 있으므로 walk에서 breath를 향하는 화살표 또한 만들어 줘야 한다.
파이썬을 배웠기 때문에 파이썬을 이용해 텀프로젝트를 진행해야 했고, 대부분은 파이썬을 이용한 데이터 분석을 주제로 하였지만, 저는 데이터 분석보다는 직접 게임을 만들어 보고 싶었기 때문에 pygame 모듈을 이용해 게임을 만들고자 했습니다. (이는 나중에 후회로 이어지지만...)
처음으로 '무'에서 '유'를 창조한 경험이였기 때문에 무척이나 힘들었지만 그만큼 성취감도 컸던 것 같습니다.
본론
이 게임을 프로그램으로 만들기 위해서는 무엇보다도 룰과 게임의 진행방식을 정확하게 파악하는 것이 중요했습니다.
그래서 먼저 룰을 조사했습니다.
< 흑과백 2 >
'흑과백 2'는 한정된 포인트를 라운드마다 나누어 대결에 사용하며 더 많은 승점을 획득한 플레이어가 승리하는 게임이다.
'흑과백 2'는 두 플레이어가 각각 99포인트 씩 가지고 시작한다.
게임은 총 아홉 라운드로 진행되며 플레이어는 라운드마다 99포인트 중 원하는 만큼의 포인트를 사용할 수 있다.
선 플레이어가 해당 라운드에 사용할 포인트를 결정하면 사용한 포인트가 한 자릿수일 경우 검은색, 두 자릿수일 경우 흰색으로 표시된다. 흑과 백 표시를 단서로 후 플레이어가 포인트를 결정하면 후 플레이어의 포인트 역시 흑과 백으로 표시되며 해당 라운드에서 더 많은 포인트를 사용한 플레이어는 승점 1점을 획득한다.
사용할 포인트는 소멸되며 남은 포인트는 5단계 표시등으로 공개된다.
99포인트 중 20포인트 씩 줄어들 때마다 한 단계씩 표시등이 꺼지게 되며 포인트를 입력한 순간 적용된다. 상대방의 남은 포인트가 0점이라도 마지막 표시등은 꺼지지 않는다.
선 플레이어가 현재 단계보다 낮아지는 포인트를 사용했다면 후 플레이어가 사용할 포인트를 결정하기 전에 표시된다.
9라운드 종료 시 승점이 더 높은 플레이어가 승리하며 게임 도중 한 플레이어가 승점 5점을 먼저 획득하면 그 즉시 해당 플레이어의 승리로 게임이 종료된다.
그러고 나서는 input을 이용해 간단하게 '흑과백2' 게임을 만들어보았습니다. (위에 적힌 룰과 조금 다를 수 있음)
player1_point = 99
player2_point = 99
player1_score = 0
player2_score = 0for i inrange(1,10):
print("Round ",i)
p1=int(input(prompt="p1)제시할 포인트를 입력해 주세요."))
while player1_point-p1<0:
p1=int(input(prompt="포인트를 초과하셨습니다. 다시 입력해주세요."))
player1_point-=p1
if p1>=0and p1<10: #한 자리 수 일 경우 "흑"표시print("Black")
else:
print("White") #두 자리 수 일 경우 "백"표시if player1_point<=79and player1_point>=60:
print("표시등이 꺼집니다.")
elif player1_point<=59and player1_point>=40:
print("표시등이 꺼집니다.")
elif player1_point<=39and player1_point>=20:
print("표시등이 꺼집니다.")
elif player1_point<=19and player1_point>=0:
print("표시등이 꺼집니다.")
p2=int(input(prompt="p2)제시할 포인트를 입력해 주세요."))
while player2_point-p2<0:
p2=int(input(prompt="포인트를 초과하셨습니다. 다시 입력해주세요."))
player2_point-=p2
if p2>=0and p2<10:
print("Black")
else:
print("White")
if player2_point<=79and player2_point>=60:
print("표시등이 꺼집니다.")
elif player2_point<=59and player2_point>=40:
print("표시등이 꺼집니다.")
elif player2_point<=39and player2_point>=20:
print("표시등이 꺼집니다.")
elif player2_point<=19and player2_point>=0:
print("표시등이 꺼집니다.")
#승점 계산if p1>p2:
player1_score+=1print("p1 승점 1점 추가")
elif p1<p2:
player2_score+=1print("p2 승점 1점 추가")
else:
print("동점입니다.")
if player1_score>player2_score:
print("p1 승")
if player1_score<player2_score:
print("p2 승")
시점 뷰에 대해 공부했다. 명암을 넣을 때는 햇빛이 어디에 있는지 인지해야한다. 사이드 뷰에서 원래의 얼굴 형태를 알맞게 살짝 옆으로 치우치게 하고, 탑 뷰에서는 아래로 치우치게 하면 훨씬 자연스러워 진다.
이렇게 말이다.
게임에서는 살짝 옆으로 보는 눈을 많이 쓴다고 한다.
캐릭터를 만들 때에는 먼저 기본 틀을 만들어준다. 형태를 정한 뒤에 머리카락을 그리기 시작한다. 귀는 머리카락을 이용해 구렛나루를 만듦으로써 만들 수 있다. 팔은 손 먼저 그리고 연결하여 완성한다. 다리는 발을 크게 그리거나 아예 생략하거나 한다. 손은 허리높이에 위치한다. 반사광으로 인해 생기는 머리카락 하이라이트까지 그려주면 완성이다.
이렇게 왼쪽을 보는 캐릭터를 하나 그려주면 수평 반전으로 오른쪽을 바라보는 캐릭터도 금방 완성할 수 있다.
2D게임을 만들 때 사용할 간단한 캐릭터도 그려보았다.
만들 게임의 컨셉이 감옥이라 죄수 캐릭터를 그리긴 했는데 컨셉을 유지할 지 말지 현재 고민 중에 있다.
일단 이 간단하고 귀여운 캐릭터를 가지고 간단한 애니메이션들을 만들어 보았다.
먼저 눈 깜빡거리기
그 다음은 걷기
마지막으로 가만히 있을 때 숨쉬기
숨 쉬는 애니메이션을 그리는 방법은 머리를 먼저 움직인 다음 몸을 움직이게 하면 된다. (머리 - 몸 순서)
코드를 실행해보면 print의 값이 40으로 나오는 것을 알 수 있다. array2[1,3]는 두 배열중 인덱스 1에 해당하는 배열에서의 인덱스 3의 값을 의미한다.
그런데 배열은 한번 생성되면 배열의 크기가 고정되어 원소를 늘리거나 줄일 수 없다.
이러한 단점을 극복하는 것이 컬렉션(리스트, 큐, 스텍, 해시테이블, 딕셔너리, 어레이리스트)이다.
//ArrayList
ArrayList arrayList = new ArrayList();
arrayList.Add(1); //arrayList에 1이라는 원소를 추가함
arrayList.Add("가나다라");
print(arrayList.Count); //arrayList에서는 length가 없고, Count가 그 기능을 해줌
arrayList[0] = 3; //배열과 사용법은 같음
arrayList.Remove("가나다라"); //직접 지움
arrayList.RemoveAt(0); //인덱스를 통해 지움
ArrayList는 누군가가 이미 만들어 놓은 class이다. 점(.)을 찍으면 Add나 Remove같은 메소드를 이용할 수 있다.
//List
List<int> list = new List<int>(); //ArrayList와 달리 특정한 자료형만을 원소로 가짐
ArrayList는 자료형 상관없이 원소를 가지지만 List는 특정한 자료형을 지정하고, 그 자료형만을 원소로 가진다. 단순히 생각하면 범위가 넓은 ArrayList를 항상 쓰면 더 편하고 좋겠다고 생각되겠지만 범위가 넓은만큼 값이 들어올 때 마다 넓은 Box를 만들고 들어올 때, 나갈 때마다 더 많은 연산이 필요해진다. 즉, 비효율 적인 것이다. 그래서 List보다 더 연산량이 많을 것이고 과부하에 걸리기 쉽다.
//HashTable
HashTable hashTable = new HashTable();
hashTable.Add("만", 10000); //(key, value) 입력
hashTable.Add("백만", 1000000);
print(hashTable["만"]); //인덱스 대신 key로 value를 호출한다//Dictionary
Dictionary<string, int> dictionary = new Dictionary<string, int>(); //key(string), value(int)
HashTable과 Dictionary의 관계는 ArrayList와 List의 관계와 동일하다고 보면 된다.
//Queue
Queue<int> queue = new Queue<int>();
queue.Enqueue(5); //원소 5를 넣음
queue.Enqueue(6); //원소 6을 넣음
queue.Dequeue(); //가장 먼저 들어간 원소를 꺼냄
queue.Dequeue(); //두 번째로 들어간 원소를 꺼냄
이 상태에서 queue.Dequeue();를 한번 더 실행하게 된다면 오류가 뜰 것이다. 왜냐면 이미 들어간 총 두개의 원소가 이전에 다 꺼내졌기 때문에 더이상 꺼낼 원소가 없기 때문이다. 그렇기 때문에 queue.Dequeue는 queue.Count!=0일 때에만 실행될 수 있도록 if문과 함께 쓰이는 것이 좋다.
queue는 선입전출이라하고 반대로 후입전출이라고 불리는 stack도 있다.
stack은 queue와 반대로 가장 마지막에 넣었던 원소를 꺼낸다.
Stack<int> stack = new Stack<int>();
stack.Push(1); //stack에 1을 넣어줌
stack.Push(2);
stack.Push(3);
stack.pop(); //3을 꺼냄