매번 구글 플레이 서비스를 유니티에 연동할 때 단번에 성공한 적이 없는거 같네요... 시간낭비가 커서 이번에 파이어 베이스를 연동하며 같이 기록을 남기고자 합니다. 

 애초에 이런 기록을 남기는게 블로그 시작의 이유였는데... 이제서야 하다니.... 

 여기서는 구글 플레이 서비스 인증과 파이어 베이스 인증, 데이터 베이스 연동까지 진행하는 플로우와 발생했던 이슈들에 대해 포스팅하겠습니다.


우선 환경은 아래와 같습니다.

  1.  Unity 2017.4.1f1
  2.  GooglePlayGamesPlugin-0.9.50
  3.  firebase_unity_sdk_4.5.2


1.  구글 플레이 서비스 와 파이어 베이스 환경설정 

  1. Google Play Console
    1. 구글 콘솔에 새 프로젝트 생성.
    2. 해당 프로젝트의 패키지로 서명된 앱을 샘플앱으로 우선 등록.
      • 패키지 등록 및 SHA-1 값을 편하게 보기 위해 ㅎㅎ ( KeyTool로 해도 되지만 개인적으로 이게 편합니다.)
  2.  Console에 게임 서비스에 프로젝트 등록.
    1. 플레이 콘솔에서 등록했던 프로젝트를 게임서비스에 등록. (Android)
    2. 파이어 베이스에서 인증에 사용하기위해 웹 어플리케이션으로도 등록.
      1. 웹 어플리케이션에 등록하는 실행 URL은 파이어베이스의 URL을 사용.
        1. ex) myproject.firebaseapp.com
        2. 위의 url은 설정의 google-services.json에서도 확인할 수 있고, 웹설정에서도 확인 가능, Authentication의 로그인 방법에 승인된 도메인에서 확인가능.
  3. FireBase 프로젝트 등록.
    1. 생성시 위의 프로젝트에 연동을 하거나, 생성 후 설정에서 안드로이드 앱을 등록하여 연동.
      1. SHA 인증서 지문 = GooglePlayConsole에서 앱로드 증명서의 SHA-1 값
    2. Authentication 의 로그인 방법에 Play 게임 사용으로 설정.
      1. 이때 등록하는 클라이언트 ID와 보안 비밀은 아래 API console에서 웹 애플리케이션의 클라이언트 ID에서 구할 수 있습니다.
        1. 반드시 웹 애플리케이션의 클라이언트 ID와 비번이여야 합니다. 웹을 통해서 인증을 받아오기 때문에
  4. API Console 설정. ( https://console.developers.google.com/)
    1.   API는 이미 위에서 FireBase와 게임서비스가 연동되면서 클라이언트 ID들이 만들어져있습니다.
    2. Android 클라이언트 ID 서명 인증서 지문 확인.
      1. 이번에도 앱로드 증명서의 SHA-1 값을 등록합니다.
  5. Unity 환경 설정.
    1. Window - GooglePlayGames - Setup - Android Setup
      1. Resource Definition 과 web App client ID 등록.
        1. 리소스 정의는 게임 서비스의 리더보드나 업적에서 리소스 받기로 받을 수 있음. 만약에 두가지를 안쓴다면 밑의 기본 리소스로 등록.
        2. WebAppClientID는 게임서비스에 등록된 웹 어플리케이션 ID 등록.
      2. FireBase 프로젝트의 설정으로 들어가 google-services.json을 다운받아 유니티 프로젝트 내에 넣는다.

2. GooglePlayService 인증 연동하기 (코드)

 

    public void activePlayGame()
    {

        PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder()
                    .RequestServerAuthCode(false)
                    .Build();

        PlayGamesPlatform.InitializeInstance(config);
        // recommended for debugging:
        PlayGamesPlatform.DebugLogEnabled = true;
        // Activate the Google Play Games platform
        PlayGamesPlatform.Activate();

    }
    public void googleLogin()
    {
        Social.localUser.Authenticate((success, errorMessage) =>
        {
            if (success)
            {
                authCode = PlayGamesPlatform.Instance.GetServerAuthCode();
            }
            else
            {

               Debug.Log("google " + errorMessage);
            }

            Debug.Log("google " + success);
        });
    }
    public void googleLogout()
    {
        PlayGamesPlatform.Instance.SignOut();

    }



2. 파이어 베이스 인증 연동하기. (코드)

 - 공식 문서의 예제 코드를 거의 임용한 코드.

 - 사용 전에는 반드시 connectFirebase를 먼저 호출. 

public void connectFirebase(string _authCode) { FirebaseAuth auth = Firebase.Auth.FirebaseAuth.DefaultInstance; Firebase.Auth.Credential credential = Firebase.Auth.PlayGamesAuthProvider.GetCredential(_authCode); auth.SignInWithCredentialAsync(credential).ContinueWith(task => { if (task.IsCanceled) { Debug.LogError("SignInWithCredentialAsync was canceled."); return; } if (task.IsFaulted) { Debug.LogError("SignInWithCredentialAsync encountered an error: " + task.Exception); return; } Firebase.Auth.FirebaseUser newUser = task.Result; Debug.LogFormat("User signed in successfully: {0} ({1})", newUser.DisplayName, newUser.UserId); }); } public string getFirebaseUser() { Firebase.Auth.FirebaseUser user = auth.CurrentUser; string uid = null; if (user != null) { uid = user.UserId; } return uid; } public void firebaseLogout() { FirebaseAuth.DefaultInstance.SignOut(); }



3. 파이어 베이스 데이터 베이스(실시간) 연동하기. (코드)

  - 공식 문서의 예저코드를 거의 임용한 코드 ( 델리게이트 제외)

  - 반드시 connectDatabase를 먼저 호출

    public delegate void DelGetDB(object data);
    DatabaseReference reference;

    public void connectDatabase()
    {
        // Set up the Editor before calling into the realtime database.
        FirebaseApp.DefaultInstance.SetEditorDatabaseUrl("https://myproject.firebaseio.com/");

        // Get the root reference location of the database.
        reference = FirebaseDatabase.DefaultInstance.RootReference;
    }
     public void FuncGetDB(object _data)
    {
        Debug.Log("SUCESS " + _data.ToString());
    }
    public void setData(string _userId, string _dataName,int _value)
    {
        reference.Child("users").Child(_userId).Child(_dataName).SetValueAsync(_value);
    }
    
    public void getData(string _userId, string _dataName, DelGetDB _funcDB)
    {
        reference.Child("users").Child(_userId).Child(_dataName).GetValueAsync().ContinueWith(task => {
            if (task.IsCompleted)
            {
                DataSnapshot snapshot = task.Result;
                _funcDB(snapshot.Value);
            }
            else if (task.IsFaulted)
            {
                // Handle the error...
                Debug.Log("getData fail ");
            }else
            {
                Debug.Log("getData cancel ");
            }
        });
    }





* 발생 오류들과 해결과정

 - 저한테는 요게 이 포스팅의 핵심입니다 ㅎㅎ


1) duplicate files copied in APK ~~~ (빌드 에러)

  - 사실 이전에 다른 프로젝트하면서 구글 플레이 서비스 인증을 사용했던 적이 있던터라...패키지들을 한꺼번에 추가했다가 발생했던 오류입니다.

  - 원인 - 패키지 임포트 간의 다른 폴더에 중복된 파일 추가로 추정.

  - 해결 - GooglePlayGamesPlugin-0.9.46 에서의 버그로, 0.9.50에서 수정되었다하여 버전 업 후 전체 패키지 파일 제거하여 차례로 다시 import


2) 구글 플레이 게임 로그인 시도시 Authentication fail 발생.

  - 솔직히 구글은 인증 실패시 원인같은거 안알려주고 error 코드로 Authentication fail로 알려줘서 이 부분은 원인과 해결방안이 굉장히 많겠지만... (Android Native로 개발해도 error code 하나 툭 던져주고 끝이라 고생했던 기억이...ㅠ)

  - 원인 - API 서비스의 Android Client ID의 서명 인증서가 앱 서명 인증서로 되어있었음.

  - 해결 - 업로드 인증서의 SHA-1 값으로 변경.


3) 구글 플레이 게임 로그인 시도시 Authentication fail 발생.

  - 네 위랑 똑같에요... 하지만 위와 상황은 달랐습니다.

  - 위는 순전히 구글 게임 인증만 시도했을 때고, 3)은 FireBase에서 사용하기 위한 AuthCode를 요청하는 코드를 추가했을 때 발생한 문제입니다. RequestServerAuthCode <- 요거

  - 원인 - Window - GooglePlayGames - Setup - Android Setup 의 웹 어플리케이션 ID에 오타가....

 - 해결 - 오타 수정.


4) FireBase 연동이 동작하지 않음

  - 원인 - 1)을 해결하며 가이드대로 Unity프로젝트에 넣었던 google-service.json을 같이 제거하고 다시 넣지 않음.

  - 해결 - 다시 추가.


5) DB에서 데이터를 가져올 때 제대로 데이터를 가져오지 못함.

  - 원인 - 가이드 그대로 코드했다가 별도 thread라는 걸 인지 못함. 지역 변수를 다른 스레드로 도는 함수내에서 조작하려 해서 발생.

  - 해결 - 델리게이트를 콜백 함수로 넘겨서 실행.


6) 리더보드 호출시 동작하지 않음

  - 상세 - 로그인과 리더보드 진입은 되나, 리더보드의 상세 등수를 보려하면 꺼짐.

  -  에러 코드 - There is no linked app associated with this client ID.

  - 원인 - 구글 콘솔 서비스(연결된 앱)의 안드로이드 클라이언트 ID와 API 서비스의 안드로이드 클라이언트 ID가 다름 ( 유니티에 ClientID를 잘못 등록해서 발생한걸로 추정)

  - 해결 - 콘솔 서비스의 연결된 앱에 새로 안드로이드를 등록하면 자동으로 API 서비스에 생성되니, 삭제 후 새로 생성.


Posted by 검은거북

 회전 로직을 너무 쉽게 생각했었네요.. 생각보다 어려웠습니다. 모든 타일을 왼쪽 상단을 기준으로 배열을 잡으려고 했는데, 막상 그렇게 만들어 놓으니 기존에 보던 테트리스 게임하고 괴리감이 커서 다시 수정을 하고, 그렇게 하니 또 회전 로직이 걸리고...ㅠ  회전 로직이 맘에 좀 안들지만... (다른 분들은 어찌했나 궁금하네요..) 이전 포스팅대로 대부분의 로직은 Block에서 거의 진행을 하고, GameManager는 전체적인 진행과 키입력에 따른 요청을 하고있습니다.

 Block의 소스가 중요로직이라고 생각되므로 Block과 키입력 예시부분만 포스팅을 하도록하죱. 


1. Block 

 - 블럭의 이동과 회전을 관리.

public class Block {
    public int[,,] tile;
    public Color32 color;
    
    int direct;
    int posX;
    int posY;

    public Block(int[,,] _tile, Color32 _color)
    {
        tile = _tile;
        color = _color;
    }
    public void SetPostion(int _x,int _y,ref int[,] _map)
    {
        int posX = _x;
        int posY = _y;

        MoveTile(direct, _x, _y, ref _map);
    }

    // 좌우로 움직임을 명령하는 함수. 외부에서 호출
    public void MoveHorizon(int _x, ref int[,] _map)
    {
        if (CheckTile(_x, _map))
        {
            MoveTile(direct,_x, 0,ref _map);
        }
    }

    // _x,_y 좌표로 이동하는 함수 ( 블럭을 이동시킬 때 사용)
    void MoveTile(int _direct, int _x, int _y, ref int[,] _map)
    {
        for(int i = 0; i < 4; i++)
        {
            for(int j = 0; j < 4; j++)
            {
                if(tile[direct, i, j] == 1)
                {

                    _map[posY + i, posX + j] = 0;
                }
            }
        }
        direct = _direct;
        posX += _x;
        posY += _y;

        for (int i = 0; i < 4; i++)
        {
            for (int j = 0; j < 4; j++)
            {
                if (tile[direct, i, j] == 1)
                {

                    _map[posY + i, posX + j] = tile[direct, i, j];
                }
            }
        }

    }

    // 좌우 좌표로 이동가능한지 확인하는 함수
    bool CheckTile(int _x, int[,] _map)
    {
        int tempX = posX + _x;
        for (int i = 0; i < 4; i++)
        {
            for (int j = 0; j < 4; j++)
            {
                if (tile[direct, i, j] == 1 && !(_map[posY + i, tempX + j]==0 || _map[posY + i, tempX + j] == 1))
                {
                    return false;
                }
            }
        }
        return true;
    }


    // 블럭을 회전시키고 적용하는 함수. 회전시 맵을 삐져나갈 경우 때문에 새로 확인.
    public void RotationTile(ref int[,] _map)
    {
        // 회전 - 4가 되면 0으로 변경.

        int tempDirect = (direct +1) & 3;
        int count = CheckRotation(tempDirect,0, _map);
        if (count == -10)
        {
            return;
        }
        else if (count != 0)
        {
            // 옆 칸으로 이동해서 재확인.
            if (CheckRotation(tempDirect, count, _map)==0)
            {
                MoveTile(tempDirect, count, 0, ref _map);
            }
        }else
        {
            MoveTile(tempDirect, 0, 0, ref _map);
        }
    }

    // 블럭이 회전 가능한지 확인하는 함수. 회전했을때 가로로 겹쳐지는 길이를 반환한다.
    // 겹치는게 없다면 rotation을 하고, 겹쳐진다면 겹쳐진 길이만큼 이동시켜 재확인.
    // 1,1을 기준으로 왼쪽이 겹치면 +, 오른쪽은 -
    int CheckRotation(int _direct,int _x,int[,] _map)
    {
        int maxLeft = 0;
        int maxRight = 0;
        for (int i = 0; i < 4; i++)
        {
            int left = 0;
            int right = 0;
            for (int j = 0; j < 2; j++)
            {
                if (tile[_direct, i, j] == 1)
                {
                    int tempX = posX + _x + j;
                    if(tempX < 0)
                    {
                        left++;
                        continue;
                    }
                    if (!(_map[posY + i, tempX] == 0 || _map[posY + i, tempX] == 1))
                    {
                        left++;
                    }
                }
            }
            for (int j =3; j >=2; j--)
            {
                if (tile[_direct, i, j] == 1)
                {
                    int tempX = posX + _x + j;
                    if (tempX >= 12)
                    {
                        right++;
                        continue;
                    }
                    if (!(_map[posY + i, tempX] == 0 || _map[posY + i, tempX] == 1))
                    {
                        right ++;
                    }
                }
            }

            if (left > maxLeft)
            {
                maxLeft = left;
            }

            if(right> maxRight)
            {
                maxRight = right;
            }
        }
        if(maxRight == 0)
        {
            return maxLeft;
        }else if (maxLeft ==0)
        {
            return maxRight * -1;
        }else
        {
            return -10;
        }
    }
    // 블럭의 밑이 고정타일 또는 바닥인지 확인 후 바닥이 아니면 MoveTile을 이용해 내린다.
    // 바닥이면 applyGround 후 false를 리턴. - GM에서는 라인검색을 할 수 있도록.
    public bool MoveDown( int[,] _map)
    {
        for (int i = 0; i < 4; i++)
        {
            for (int j = 0; j < 4; j++)
            {
                if (tile[direct, i, j] == 1 && _map[posY + i+1, posX + j] == -2)
                {
                    ApplyGround(ref _map);
                    return false;
                }
            }
        }
        MoveTile(direct,0, 1, ref _map);
        return true;
    }

    // 바닥에 닿은 블럭을 고정타일로 맵에 반영하는 함수.
    public void ApplyGround(ref int[,] _map)
    {
        for (int i = 0; i < 4; i++)
        {
            for (int j = 0; j < 4; j++)
            {
                if (tile[direct, i, j] == 1 )
                {
                    _map[posY + i, posX + j] = -2;
                }
            }
        }
    }

    // 블럭을 바닥으로 한 번에 이동시키는 함수.
    // 블럭의 밑을 바닥까지 검사하여 가장 적은 거리만큼 이동 시키고 ApplyGround를 한다.
    public void DropTile(ref int[,] _map)
    {
        int min=30;
        for (int i = 0; i < 4; i++)
        {
            for (int j = 0; j < 4; j++)
            {
                if (tile[direct, i, j] == 1)
                {
                    int count = 0;
                    while (_map[posY + i + count +1, posX + j] != -2)
                    {

                        count++;
                    }
                    if (count < min)
                    {
                        min = count;
                    }
                }
            }
        }
        MoveTile(direct,0, min, ref _map);

    }
}



2. 키 입력에 따른 Block함수 사용 예시 ( GameManager의 일부)


        if (!isGameOver)
        {
            // 버튼 입력에 따른 테트리스 로직 실행.
            if (Input.GetKeyDown(KeyCode.RightArrow))
            {
                playerBlock.MoveHorizon(1, ref map);
            }
            if (Input.GetKeyDown(KeyCode.LeftArrow))
            {

                playerBlock.MoveHorizon(-1, ref map);
            }
            if (Input.GetKeyDown(KeyCode.UpArrow))
            {
                playerBlock.RotationTile(ref map);
            }
            if (Input.GetKeyDown(KeyCode.DownArrow))
            {
                if (playerBlock.MoveDown(map) == false)
                {
                    // 라인을 탐색하여, 가득차면 제거
                    // 천장을 넘어가면 게임 종료.
                    // 새로운 블럭생성.
                }
            }
            if (Input.GetKeyDown(KeyCode.Space))
            {
                playerBlock.DropTile(ref map);
                playerBlock.ApplyGround(ref map);
                // 라인을 탐색하여, 가득차면 제거
                // 천장을 넘어가면 게임 종료.
                // 새로운 블럭생성.
            }
        }




* 아래 동영상은 완성본의 일부 영상입니다.





* 지적은 언제나 환영입니다.

Posted by 검은거북

맵 타일과 유저 입력에 따른 전체적인 게임 플로우를 관리하는 GameManager 클래스,

블럭의 이동 및 회전, 동작을 관리하는 Block 클래스로 전체 로직을 관리하겠습니다.


전체적인 플로우는 Block 객체가 고유의 블럭 정보 ( 위치, 회전, 색 등)를 가지고, GameManager가 유저의 입력가 게임 진행에 따라 Block에게 동작을 요청하는 식으로 진행될 것입니다.


1. GameManager

public class GameManager : MonoBehaviour {
    // 사전에 블럭의 배열과 색을 정의. ( 생략)
    Block[] blocks = new Block[7];
    const int HEIGHT =25;
    const int WIDHT = 12;

    Block playerBlock;
    int[,] map = new int[25, 12];

    // Use this for initialization
    void Start () {
        // 초기화
    }
	
    // Update is called once per frame
    void Update () {
	// 버튼 입력에 따른 테트리스 로직 실행.
    }

    // 타일을 매 일정시간마다 내리는 함수.
    IEnumerator Tick()
    {
        yield return null;
    }
    // 맵 생성 함수
    void MakeMap(ref int[,] _map)
    {

    }

    // 4,4 위치에 블럭을 새로 생성
    Block MakeBlock(ref int[,] _map)
    {
        return null;
    }

    // 고정된 블럭이 맵 타일의 최상위를 넘었는지 확인하는 함수. 
    bool CheckEnd(int[,] _map)
    {
        return false;
    }


    // 고정된 타일의 좌우를 비교하여 가득 찬 라인을 제거하도록 요청한다.
    void CheckLine(ref int[,] _map)
    {

    }

    // line을 삭제하고, 위쪽의 타일을 내린다.
    void DeleteLine(int _line)
    {

    }

    void GameOver()
    {

    }

}



2. Block

public class Block { public int[,,] tile; public Color32 color; int direct; int posX; int posY; public Block(int[,,] _tile, Color32 _color) { tile = _tile; color = _color; } public void SetPostion(int _x,int _y) { int posX = _x; int posY = _y; } // 좌우로 움직임을 명령하는 함수. 외부에서 호출 public void MoveHorizon(int _x, ref int[,] _map) { } // _x,_y 좌표로 이동하는 함수 ( 좌우 이동과 한 칸씩 내릴때 사용) void MoveTile(int _direct,int _x, int _y, ref int[,] _map) { } // 좌우 좌표로 이동가능한지 확인하는 함수 bool CheckTile(int _x, int[,] _map) { return true; } // 블럭을 회전시키고 적용하는 함수. 회전시 맵을 삐져나갈 경우 때문에 새로 확인. public void RotationTile(ref int[,] _map) { } // 블럭의 밑이 고정타일 또는 바닥인지 확인 후 바닥이 아니면 MoveTile을 이용해 내린다. // 바닥이면 applyGround 후 false를 리턴. - GM에서는 라인검색을 할 수 있도록. public bool MoveDown( int[,] _map) { return true; } // 바닥에 닿은 블럭을 고정타일로 맵에 반영하는 함수. void ApplyGround(Block _block, ref int[,] _map) { } // 블럭을 바닥으로 한 번에 이동시키는 함수. // 블럭의 밑을 바닥까지 검사하여 가장 적은 거리만큼 이동 시키고 ApplyGround를 한다. public void DropTile(Block _block, ref int[,] _map) { } }





* 지적은 언제나 환영입니다.


Posted by 검은거북

1.기획

 타일 - 20 X 10

  1. 블럭은 테트리스의 기본 블럭 7가지 ( ㅁ,ㄴ,ㄱ,ㅣ,ㅜ,z, 반대 z)
  2. 블럭은 랜덤하게 맨위의 중앙에서 생성된다.
  3. 매 일정 시간마다 블럭은 한 칸씩 내려온다.
  4. 화살표 좌,우 에 따라 블럭은 한 칸씩이동하고, 위는 블럭을 오른쪽으로 회전, 아래는 블럭을 빠르게 내린다.  spacebar는 바로 맨 아래로 블럭을 내린다.
  5. 하나의 줄이 가득차면 제거되고, 그 위의 칸이 모양 그대로 내려온다.
  6. 바닥에 고정된 블럭이 맨 위에 닿으면 종료된다.


2. 플로우

  1.  타일 생성
    1.  테트리스는 블럭이 좌우로 계속 움직이므로 움직일 수 있는 칸을 한정할 수 있도록 기존 타일에 +2 씩 하여 막을 공간을 만든다. ( 22 X 12 )
      1. 막는 타일은 -1로 값을 배정한다. ( 바닥 타일은 -2로 고정타일 취급)
      2. 위쪽 공간은 타일이 가득찼을 경우를 대비해 최대 4만큼 여유공간을 두고, 다른 값 -3로 배정하여, 고정된 타일이 -3에 닿았는지를 보고 종료를 판단한다.
      3. 고로 최종 배열은 25 X 12
  2. 블럭 생성
    1.  블럭의 최대길이가 4 이므로 각 블럭은 4X4배열에 저장. - 구조체?
    2.  7개의 블럭을 4방향 회전했을때의 모양을 미리 7X4 배열에 각각 저장.
    3.  배열의 생성은 맵 타일의 4,4 위치에 생성.
  3. 유저 조작
    1.  좌 우 버튼
      1. 이동 할 위치의 맵 배열과 블럭배열을 비교하여 겹치지 않는다면, 이동한다.
        1. 배열을 좌우로 3씩 더 늘린다.
        2. 맵 배열을 비교할 때 -1(벽)과 만나면 다음 밑에 칸으로 넘어간다.
        3. 이동 - 맵 배열에서 현재 블럭 배열의 1(블럭값)의 위치를 비우고, 이동하여 값을 채운다.
    2.  회전 버튼 
      1.  블럭 배열에서 회전 인덱스를 +- 하여 회전한 배열을 위와 동일하게 비교하여 겹치지 않는다면 이동한다. 
      2.  회전시 블럭이 맵배열을 벗어난다면? - 맵 배열의 벽(-1)을 만났을때부터 count를 세고, 맵을 벗어난 블럭 길이만큼 count++을 하여, count만큼 반대로 이동시킨다. 
    3.  떨구기
      1. 블럭 배열의 인덱스가 존재하는 열을 맵배열에서 검색하여 고정 배열(-2)를 처음 만나는 count값을 구해 그만큼 내려주고, 고정블럭으로 변환한다.
  4. 블럭 이동 및 고정
    1. 블럭은 매 일정시간마다 내려온다.
      1. 블럭배열의 다음 이동 칸(아래)과 맵 배열을 비교하여 겹치지 않는다면 이동.
    2. 블럭 고정
      1. 만약 비교시 블럭배열과 맵배열의 -2(고정배열)이 겹친다면, 현재 위치에서 고정배열로 값을 변경하고, B로 돌아간다.
      2. 고정 후 맵 타일의 최상위를 검색해 -3이 아닌값이 있으면 종료로 판단.
    1. 라인제거
      1. 고정 배열로 변경시 고정배열들을 비교하여 행 전체가 -2(고정배열) 이면 해당 행을 제거한다. (0으로 변경)
      1. 라인이 제거되면 제거된 라인의 위 고정블럭들(-2)들은 제거된 count만큼 일정하게 내린다.
        1. 중간이 제거안될 경우는? - 라인 제거는 한 줄씩 요청하고, 가장 위 타일부터 제거를 요청한다. 하나의 함수에서 제거와 그 위의 타일을 내리는 작업까지 한다.
  5. 고정 블록이 최상위 공간을 벗어날때까지 B~D 반복





* 지적은 언제나 환영입니다.

Posted by 검은거북

간혹 코딩을 잘못해서 무한루프가 돌거나, 유니티가 강제로 꺼질 때가 있습니다. 근데 이 때 씬을 저장 안했다면....그대로 날라가죠...ㅠㅠ  비슷한 경험이 있어서 알아보니까 유니티에서  씬이 변경될 때 별도로 백업을 해준다고 하더라고요. 


만약 무한루프나 모종의 이유로 강제종료가 되었다면, 백업된 씬의 확장자를 Unity로 변경해서 실행시키면 됩니다.

단, 위의 작업은 유니티를 재실행하기 전에 해야합니다. 재실행하면 백업도 날라가요.


백업 파일 경로 - [UnityProject] - [Temp] - [_Backupscenes]


예 ) 해당 파일 경로의 0.backup -> 0.Unity로 이름 변경

   0.Unity를 실행시켜 종료되기 이전의 씬을 불러오고, 다시 기존의 씬에 저장.

Posted by 검은거북

 지렁이가 지상으로 올라갔다 내려갔다 하다보니까 오브젝트가 겹치는 부분을 좀 더 신경써야하더군요...ㅠ (예를들면 먹이는 지상에있고, 지렁이가 밑을 기어가는 경우)

 때문에 기존에 map 배열에 값을 넣는 방식이 아닌 bit 단위로 And와 OR 연산을 하도록 변경되었습니다.

 중요 로직은 매 일정시간 단위로 지렁이와 적을 움직이는 함수....정도네요. 


1. GameManager (Tick)

    public IEnumerator Tick()
    {
        bool isGameOver = false;
        while (snake.Count < 16)
        {
            // 현 시점의 지상으로 나왔는지 여부.
            bool _isOut = isOut;
            Snake head = snake[0];
            isGameOver = false;

            // 지렁이의 다음 위치
            IntVector2 destination = new IntVector2(head.currentPosition.x + checkPosition[direct].x, head.currentPosition.y + checkPosition[direct].y);


            // 지렁이는 꼬리부터 몸통까지 우선 움직이고, 머리를 움직일 때 먹이와 방해물 판별을 한다.
            // 꼬리 위치가 다음 이동할 위치라면, 꼬리를 먼저 오므려주어야 꼬리에 닿지 않는다.
            map[snake[snake.Count - 1].currentPosition.y, snake[snake.Count - 1].currentPosition.x] &= 7;
            for (int i = snake.Count - 1; i > 0; i--)
            {
                snake[i].SetSnakePosition(snake[i - 1].currentPosition, snake[i - 1].isOut);
                if (snake[i].isOut)
                {
                    map[snake[i].currentPosition.y, snake[i].currentPosition.x] |= 16;
                }else
                {
                    map[snake[i].currentPosition.y, snake[i].currentPosition.x] |= 8;
                }
                StartCoroutine( snake[i].MoveSnake());
            }
            // 머리의 다음 위치가 영역을 넘어가면 종료. ( 배열을 2개 더 크게 만들어 테두리에 장애물을 두는 것도 고려)
            if (destination.x<0 || destination.y<0 || destination.x>=WIDTH || destination.y >= HEIGHT)
            {
                isGameOver = true;
                break;
            }
            // 게임 로직 판단.
           //  밖에 나온 상태에선, 먹이를 먹고, 움직이는 장애물에 부딫힌다.
           // 0 = 빈공간, 1 = 먹이, 2 = 방해물 , 4 = 움직이는 장애물 , 8 = 지렁이, 16 = 밖에 나와있는 지렁이몸체
            if (_isOut)
            {
                if (map[destination.y, destination.x] == 1)
                {
                    // 먹이를 제거하고, 지렁이의 꼬리를 복사해 꼬리 뒤에 붙여 몸체를 늘린다. 그리고 새로운 먹이를 랜덤하게 생성한다.
                    Eat(ref map,destination);
                }
                else if ((map[destination.y, destination.x] & 4) == 4)
                {
                    // 움직이는 장애물에 닿으면 게임종료
                    // 움직이는 장애물은 지상에 있으므로, 지하에 있는 지렁이 몸체와 겹칠 수 있다. And 연산을 통해 장애물만 확인.
                    isGameOver = true;
                }
            }
            if (map[destination.y, destination.x] == 2)
            {
                // 고정 장애물에 닿으면 게임종료
                isGameOver = true;
            }
            else if (map[destination.y, destination.x] >= 8)
            {
                // 자신의 몸체에 닿으면 게임종료
                isGameOver = true;
            }
            
            // 머리를 목표로 이동 및 애니메이션.
            head.SetSnakePosition(destination, _isOut);
            StartCoroutine(head.MoveSnake());
            // 지렁이는 지하에 있을때 지상에 있는 먹이와 움직이는 장애물에 겹칠 수 있다. OR연산으로 지렁이bit만 더해준다.
            if (head.isOut)
            {
                map[head.currentPosition.y, head.currentPosition.x] |= 16;
            }
            else
            {
                map[head.currentPosition.y, head.currentPosition.x] |= 8;
            }

          

            //지렁이를 움직인 다음에 적을 움직여준다.
            for (int i = 0; i < enermy.Count; i++)
            {
                // 지렁이가 밖에 나오는 상태면 그 공간의 지렁이를 목표물로 삼는다.
                if (_isOut)
                {
                    enermy[i].SetDestination(map, destination);
                }
                // 너비탐색을 통해 지렁이를 쫒는다. 만약 밖에나온 지렁이 몸체와 닿으면 종료.
                IntVector2 enermyDest = enermy[i].FindNextTile(map);
                if (map[enermyDest.y, enermyDest.x] == 16)
                {
                    isGameOver = true;
                }
                // 적이 머물렀던 자리의 bit를 0으로 만들고 이동위치의 bit에 4를 더한다.
                map[enermy[i].currentPosition.y, enermy[i].currentPosition.x] &= 27;
                map[enermyDest.y, enermyDest.x] |= 4;
                enermy[i].SetPosition(enermyDest);
                yield return StartCoroutine(enermy[i].MoveEnermy());

            }
              if (isGameOver)
            {
                break;
            }
            yield return null;

        }


        if (isGameOver)
        {
            GameOver();
        }
        else
        {
            Victory();
        }
    }


 지렁이의 길이가 16 이상이 될 때까지 매 틱마다 지렁이와 적을 움직입니다.

Map은 bit로 이루어지고, 각 비트는

0000 (0) = 빈공간, 0001(1) = 먹이, 0010 = 움직이는 장애물 , 0100 = 지렁이, 1000 = 밖에 나온 지렁이로 이루어져있습니다.

 처음에는 일반 변수로 했었는데 지하에 나오는 지렁이와 지상에 있는 장애물,먹이가 겹칠 경우 지상의 값을 덮어씌우다보니 배열을 나누거나, bit 연산으로 바꿔야 했고, 결국 bit 연산으로 변경해서 만들었습니다.

 해당 게임에서는 지렁이의 꼬리를 먼저 오므려서 지렁이가 꼬리를 먹지 못하게끔 했습니다. 

지렁이 게임에 괜히 기획 추가해서 좀 꼬였었는데, 이렇게 하는게 재밌긴하네요.. 이전에 만든 게임도 조금씩 변형을 해볼..



* 다른 로직은...포스팅에 추가할만한 건 너비탐색 정도밖에 없는 것 같네요. 너비탐색은 별도로 포스팅하는게 좋을 듯하여 지렁이 관련 로직은 여기서 마무리를 하겠습니다.

            


* 초록색 - 지렁이, 파란색 - 먹이, 빨간색 - 방해물, 분홍색 - 움직이는 방해물






* 지적은 언제나 환영입니다.

Posted by 검은거북

클래스 설계 및 함수

클래스는 전체적인 게임 플레이를 진행하고, 맵을 관리할 GameManager와 지렁이의 각 파트를 담당할 Snake, 적군을 담당할 Enermy로 이루어져있습니다.


1.GameManager

 // 전체적인 게임 플레이와 맵 관리.
public class GameManager : MonoBehaviour {

    int[,] map;
    bool isOut;
    List<snake> snake;
    List<enermy> enermy;

    // 매 틱마다 지렁이와 적군을 움직입니다.
    // 지렁이 헤드 위치에 따라 먹이를 먹거나, 게임을 종료시킵니다.
    // 지렁이를 방향으로 이동, 애니메이션 진행 후 
    // 만약 밖으로 나와있다면 적군의 경로 탐색으로 이동 좌표를 얻은다음 배열에 반영-> 애니메이션
    // 밖으로 나오는 버튼을 클릭하지 않은 상태면 목표지점에 지렁이가 밖에 있는지 확인후 위의 플로우 진행.
    public IEnumerator Tick()
    {

        yield return null;
    }
    // 맵을 랜덤하게 생성.
    public void MakeMap(ref int[,] _map)
    {

    }

    // 먹이를 먹었을 때, Snake 길이를 늘리는 함수
    public void Eat()
    {

    }

    // 버튼을 통해 지렁이의 이동방향을 변경합니다.
    // 동,남,서,북 차례로 방향 배열을 만들고, +- 1을 해준다.
    public void TurnLeftButton()
    {

    }
    public void TurnRightButton()
    {

    }

    // 지상으로 나가기 버튼 
    // 버튼을 누른동안에는 true, 아니라면 false
    public void TurnShowButton(bool isDown)
    {

    }
}


2. Snake

 // 지렁이의 각 부분을 담당하는 클래스
// 이동 좌표를 저장하고 좌표로 이동하는 애니메이션을 동작한다.
public class Snake : MonoBehaviour {
    // 이동할 좌표.
    IntVector2 currentPosition;
    // 밖으로 나왔는가
    bool isOut;


	public void SetSnakePosition(IntVector2 _current,bool _isOut)
    {

    }

    // 배정된 좌표로 이미지를 이동시킨다.
    public IEnumerator MoveSnake()
    {

        yield return null;
    }
}




3.Enermy


 // 움직이는 방해물
public class Enermy : MonoBehaviour {
    IntVector2 currentPosition;
    IntVector2 nextPosition;

    IntVector2 destination;
    int[,] findMap;

    // 목표물로부터 최단거리를 검색해 다음 이동할 타일을 반환.
    // findMap에서 주변 4방향 중 가장 작은 값이 있는곳으로 이동. (자신이 지나온곳은 100으로 변경)
    public IntVector2 FindNextTile(int[,] _mainMap,IntVector2 _destination)
    {
        return new IntVector2();
    }
    // 목표지점이 바뀌면 findMap을 새로 생성한다.(너비탐색)
    public void MakeFindMap(ref int[,] _findMap, int[,] _mainMap, IntVector2 _startPos, IntVector2 _destination)
    {

    }
    // 목표지점에 여전히 지렁이가 지상에 있는지 확인.
    public bool IsHide(int _map)
    {
        return false;
    }
    // 배정된 좌표로 이미지를 이동시킨다.
    public IEnumerator MoveEnermy()
    {

        yield return null;
    }
}




* 지적은 언제나 환영입니다.

Posted by 검은거북

고전의 고전의 고전 게임 지렁이 게임!!!  다만 지렁이 게임 자체의 로직은 단순하고 아마 검색하면 많이 나오지않을까..?싶네요. 그래서 저는 조금 룰을 추가하고, 연출을 추가해서 만들도록 하겠습니다. (별반 차이는 없을거 같지만)


1. 기획

맵 타일 16*16 (벽 타일을 고려하여 18*18)

  1.  맵은 먹이, 고정 방해물과 움직이는 방해물로 이루어져있다.
  2.  먹이는 하나를 먹으면 새로 하나가 랜덤한 곳에 배치된다.
  3.  방해물은 최초 배치된 갯수가 유지되며, 움직이는 방해물은 지렁이가 2칸 이동할때 1칸 이동한다.
  4.  지렁이는 직선으로만 이동하고, 오른쪽 왼쪽 버튼을 통해 직각으로 방향전환을 할 수 있다.
    1.  지렁이는 항상 중앙에서 위로 출발하며 시작한다.
  5.  지렁이는 평소 땅 속에 있다가 버튼을 통해 지상으로 나온다. 지상으로 나오면 먹이를 먹을 수 있지만, 움직이는 방해물이 쫒아온다.
  6.  움직이는 방해물은 지렁이가 땅속에 있는 동안에는 추적하거나 공격하지 못한다. (지나가도 죽지않는다.
  7.  고정 방해물은 지렁이가 땅속에 있어도 부딪히면 죽는다.
  8.  지렁이는 최초 길이가 2이고, 먹이를 먹으면 길이가 1 늘어나며, 방해물에 닿거나 자신의 몸에 닿으면 게임이 종료된다.
  9.  지렁이의 몸체 길이가 16 이상이면 새 스테이지로 넘어간다.
  10. 스테이지에 따라 게임속도와 장애물 갯수가 늘어난다.(최대 2배속 ,장애물 20개)



2. 플로우

유저

  1.  맵은 랜덤하게  생성한다. ( 먹이는 1개, 방해물은 고정 방해물 8~12개, 움직이는 방해물은 1개)
    • 하나의 맵 배열에 이동가능은 0 , 먹이는 1, 방해물은 2, 움직이는 방해물은 3으로 생성.
    • 먹이나 지상의 방해물은 지하의 지렁이와 겹칠 수 있다. bit 단위로 맵을 관리.
      • 이동가능 - 0 , 먹이 - 1 , 방해물 -2, 움직이는 방해물 -4, 지렁이 -8, 지상지렁이 -16
      • 겹칠때는 &, | 연산을 통해 해당 bit만 더하고 뺀다.
  2. 지렁이는 특정 방향으로 매초마다 움직인다. 
  3. 유저가 좌, 우 버튼 클릭시 지렁이는 해당 방향의 직각으로 이동하고, 지상 버튼 클릭하는 동안에는 지상으로 나와서 이동한다. 
    • 1) 지렁이는 좌표 리스트로 이루어진다. 리스트의 맨 뒤가 꼬리, 맨 앞이 머리이다. 
      • 이동시에는  리스트의 맨 앞의 좌표(꼬리)를 제거하고, 맨 뒤에 다음 이동할 좌표를 추가한다. 
      • 이동시에는 리스트의 맨 뒤(꼬리)부터 맨 앞(머리)까지 자신보다 뒤에있는 좌표를 배정하고, 이동 애니메이션을 실행한다. 
      • 새로 추가되는 머리는 리스트의 마지막 좌표에 현재 향하고 있는 방향을 더한다. (동서남북 중 하나)
      • 일정 시간마다 다음 칸으로 이동을 진행한다. (코루틴)
        • 몸통과 머리, 꼬리 좌표를 배정하고 난뒤 각 오브젝트는 다음 이동 좌표로 이동한다. ( 러프 이동)
        • 이동하는 애니메이션을 위해 지렁이의 몸체는 각각 오브젝트로 사용.
      • 먹이를 먹으면 길이를 1늘리며, 리스트 맨 뒤에 기존의 꼬리와 동일한 오브젝트를 추가한다.
      • 이동은 180도로 할 수 없어야하므로, 플래그를 두어 좌 우 버튼은 매 틱 한 번만 클릭이 되도록한다.
    • 2) 지렁이를 큐를 이용해 만든다. 
      • 꼬리가 제거되고, 머리가 추가되는 식으로 한다면, 큐가 맞지만, 각 몸통이 다음 타일로 이동하는 애니메이션을 진행하려면 큐는 맞지않아 보임.
  4.  먹이를 먹으면 길이가 1씩 늘고, 길이가 총 16이 되면 다음 스테이지로 이동하고, 자신의 몸이나 방해물에 닿으면 종료된다.
    1. 먹이를 먹을때 종료 판단. 이동시 배열의 값에따라 종료 판단.
움직이는 방해물
  1.  평소에는 배정된 타일에 가만히 있다가 지렁이가 밖으로 나오면 지렁이의 1/2 속도로 쫒아온다.
    1. 지렁이가 밖으로 나온는 버튼을 유저가 클릭하면 지렁이의 머리가 있는 타일을 움직이는 방해물에게 전달한다.
      • 전달된 경로를 향해 최단경로 탐색을 진행한 후 다음 타일을 저장 후, 이동한다.
      • 최단 경로 탐색 알고리즘은 무엇을?
        • A* 알고리즘 - 휴리스틱이 의미없다
        • 다익스트라
        • 플로이드
        • 너비 탐색 - 경로간 가중치도 없으니 너비탐색으로 하겠습니다.
          • 목표지점부터 시작지점까지 너비 탐색을 하여  칸마다 +1을 하여 별도의 맵에 저장한다. (시작에 도착하면 종료)
          • 시작지점에서는 저장된 맵을 보고 4방향 (자신이 지나온곳을 제외한) 중 가장 작은 값으로 이동한다. 





* 지적은 언제나 환영입니다.

Posted by 검은거북

Flood 영역을 탐색하는 로직은 아래와 같습니다.


1. SearchFlood

 
public void SearchFlood(ref int[,] _tileArray, ref bool[,] _tileSelect, int _searchTile)
    {
        IntVector2[] checkPosition =
        {
            new IntVector2(1,0),
            new IntVector2(0,1),
            new IntVector2(-1,0),
            new IntVector2(0,-1)
        };
        bool[,] visitArray = new bool[HEIGHT,WIDTH];
        IntVector2 tile = new IntVector2(0,0);

        Queue<intvector2> tileQueue = new Queue<intvector2>();
        tileQueue.Enqueue(tile);
        visitArray[tile.y, tile.x] = true;

        while (tileQueue.Count > 0)
        {
            tile = tileQueue.Dequeue();

            // 큐에서 꺼낼 때 타일을 변경한다.
            _tileArray[tile.y, tile.x] = _searchTile;
            _tileSelect[tile.y, tile.x] = true;

            for (int i = 0; i < 4; i++)
            {
                IntVector2 checkTile = new IntVector2(tile.x + checkPosition[i].x, tile.y + checkPosition[i].y);

                // 예외처리 위치가 벗어났는지, 이미 접근했던 곳인지
                if (checkTile.x<0 || checkTile.y <0 || checkTile.x>=WIDTH || checkTile.y>=HEIGHT || visitArray[checkTile.y,checkTile.x] == true)
                {
                    continue;
                }
                // 접근했다는 표식을 남긴다.
                visitArray[checkTile.y, checkTile.x] = true;

                // 선택영역이거나 클릭된 타일이면 큐에 넣는다.
                if(_tileSelect[checkTile.y,checkTile.x] == true)
                {
                    tileQueue.Enqueue(checkTile);
                }else if(_tileArray[checkTile.y, checkTile.x] == _searchTile)
                {
                    tileQueue.Enqueue(checkTile);
                }
            }

        }
    }

 최초 (0,0)의 타일을 큐에 넣고, 큐에서 하나씩 꺼내어 4방향으로 탐색하여, 선택영역이거나 클릭된 타일과 동일한 타일이면 큐에 넣습니다. 큐에 더 이상 타일이 없을 때까지 반복하여 4방향 탐색을 진행합니다.( 너비탐색이죠.)

 만약 2번째 방식 (테두리를 별도로 저장)으로 하려면 선택 영역이거나 클릭된 타일을 큐에 넣는 곳에서 그 외일때 tile 위치를 테두리 큐에 별도로 저장하면 되겠네요.


 * 위의 함수는 최초 생성 후에 최초 선택영역을 지을때도 사용가능합니다. (0,0)타일을 매개변수로 넘기면 되니까요. 


* 아래는 오브젝트와 연출이 추가된 버전입니다.





* 지적은 언제나 환영입니다.

Posted by 검은거북

기획

타일은 5개, 칸 수는 16 * 16

1. 타일은 랜덤하게 생성된다.

2. 왼쪽 위의 첫번째 타일을 선택 영역으로 정하고, 게임이 진행된다.

3. 타일을 선택하면 선택 영역이 선택된 타일로 변경된다. 선택 영역의 이웃(4방향)에 선택한 타일과 동일한 타일의 영역이 있을경우 해당 영역도 선택 영역이 된다.

4. 타일은 변경시 왼쪽 위를 기준으로 동,남 방향으로 변경된다.

5. 전체 타일이 선택 영역이 되면 종료된다.

  



플로우

1. 각 칸에 랜덤하게 타일을 생성한다.

2. 최초 타일 생성 완료시 왼쪽 위의 첫 타일을 기준으로 차례로 비교하여 선택 영역을 설정한다.

 2-1) 자신을 기준으로 4방향으로 탐색하여 자신과 동일하면, 큐에 넣고 선택영역으로 설정한다. 큐에 타일이 존재하면 위의 작업을 반복한다.

3. 타일 클릭시 선택 영역과 선택 영역의 이웃한 타일 중 클릭타일과 동일한 영역을 클릭한 타일로 변경하고, 선택 영역으로 설정한다.

3-1) 왼쪽 위 첫 타일을 시작점으로 4방향 탐색을 하여, 선택 영역이거나, 클릭한 타일과 같으면 큐에 넣으며 선택 영역으로 체크, 한 사이클 완료시 큐에서 차례로 빼서 동일한 로직 진행. ( 접근했던 타일은 별도의 배열에 표시한다. - 중복되게 큐에 넣지 않기 위해)


3-2)  선택 영역의 테두리를 별도로 저장해놓는다. 선택 영역의 테두리만을 큐에 넣고, 큐를 1번과 같이 4방향 탐색과 큐를 이용하여 선택 영역으로 변경한다.  선택 영역에 대해 타일을 변경한다.

    - 테두리 판단 - 큐에서 꺼내어 4방향 판별을 할 때, 한 방향이라도 선댁영역, 클릭된 타일이 아니라면 테두리로 판단.


 * 3-1 선택 - 3-2는 선택 영역의 비교를 줄이고자 제안했으나, 타일을 변경하는 과정에서 결국 동일한 비교문이 사용된다. 비교적 간단한 로직 선택


4. 전체 영역이 선택 영역이 되면 종료된다.






* 지적은 언제나 환영입니다.

Posted by 검은거북
이전버튼 1 2 3 4 이전버튼

블로그 이미지
프로그래밍 공부 요약 및 공부하며 발생한 궁금증과 해결과정을 포스팅합니다.
검은거북

공지사항

Yesterday
Today
Total

달력

 « |  » 2025.4
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30

최근에 올라온 글

최근에 달린 댓글

글 보관함