이번 내용은 캐릭터 구현!

이라고는 하지만 실질적으로 구현된 것은 길찾기, 시설 사용하기 정도? 네요.

 

아래와 같은 것을 만드는 겁니다.

 

 

 (초록색 캐릭은 Gym 시설을 사용하는 마을 캐릭터고, 일반 캐릭은 Sell시설을 사용하는 관광객입니다.)

 음... 원래 시설이 캐릭터가 시설을 사용할 때의 애니메이션 인덱스를 가지고 캐릭터가 사용할 때 지정을 해줘야하는데... 현재 만들어진 애니메이션이 없어서 진행하지 않았습니다.

 

 일단 이전 포스팅들에 비해 약간 변경이 된 점이 시설 설치시 시설이 너비탐색을 통해 자신의 로드맵을 생성합니다. 그리고 캐릭터는 목표 시설의 로드맵을 보고 이동하게 됩니다. ( 사실 길찾기의 경우 다양한 길찾기 방법을 시도해 보면서 공부하려고 생각 중입니다. 그래서 초창기에 별도로 정해두지 않았습니다.)

 

 캐릭터의 State는 Idle, Move, Job으로 나뉘어집니다. 최초에는 Idle 상태로 랜덤한 방향으로 서성거리다가 목표 시설(Fame 순으로 빈 시설 탐색) 발견시 Move 상태로 변경되어 목표시설로 이동합니다. 시설에 들어가는 순간 Job 상태가 되고, 시설을 나오면 Idle로 전환됩니다.

 

Character

 

public abstract class Character : MonoBehaviour {      CharacterState state ;     int money = 150;      int maxHp = 100;     int hp;     int str = 100;     int spd = 100;          Facility useFacility;      protected TileController tileController;     protected FacilityController facilityController;      Tile currentTile;     Tile destTile;     Tile nextTile;      protected Facility beforeFacility;     public Animator anim;          public string textPoolName = "TextPool";      void Start()     {         tileController = GameObject.Find("TileController").GetComponent<TileController>();         facilityController = GameObject.Find("FacilityController").GetComponent<FacilityController>();         hp = maxHp;         anim = GetComponent<Animator>();         state = CharacterState.stateIdle;         currentTile = tileController.getTile(0, 0);         nextTile = state.FindNext(this, tileController);     }     void Update()     {         if (state.Move(this, nextTile))         {             currentTile = nextTile;             state = state.CheckTile(this);              nextTile = state.FindNext(this, tileController);         }     }     public void setDestTile(Tile _dest)     {         destTile = _dest;     }     public Tile getDestTile()     {         return destTile;     }     public Tile getCurrentTile()     {         return currentTile;     }      public void addHp(int _hp)     {         hp = Mathf.Min(maxHp, hp + _hp);          ShowMoveText("Hp " + _hp);     }     public bool isFullHp()     {         return hp >= maxHp;     }     public int getHp()     {         return hp;     }     public void addMoney(int _moeny)     {         money += _moeny;          ShowMoveText("Price " + _moeny);     }     public bool isEmptyMoney(int price)     {         return money-price >= 0;     }     public int getMoney()     {         return money;     }     public void addStr(int _str)     {         str += _str;         ShowMoveText("Strong " + _str);     }     public int getStr()     {         return str;     }     public void addSpd(int _spd)     {         spd += _spd;         ShowMoveText("Speed " + _spd);     }     public int getSpd()     {         return spd;     }      // 건물에 들어가 애니메이션 동작. (건물별로 애니메이션 번호)     public void EnterFacility(Facility _facility)     {         beforeFacility = _facility;         useFacility = _facility;         useFacility.EnterFacility(this);          state = CharacterState.stateJob;          StartCoroutine(UseFacilityInTime(useFacility.getUseTime()));     }     IEnumerator UseFacilityInTime(float time)     {         yield return new WaitForSeconds(time);         while (useFacility.InteractWithCharacter(this))         {             yield return new WaitForSeconds(time);         }         ExitFacility();     }     public void ExitFacility()     {         useFacility.ExitFacility();         useFacility = new NullFacility();                   state = CharacterState.stateIdle;         nextTile = state.FindNext(this, tileController);     }      private void ShowMoveText(string _text)     {         MoveText textObj = PoolManager.Instance.getObject(textPoolName,transform).GetComponent<MoveText>();         textObj.setTextMessage(_text);         textObj.transform.position = transform.position + new Vector3(0, 0, -1);         textObj.gameObject.SetActive(true);     }      public abstract Tile FindDestination(); }  

 

어우 길어... 외부에서 접근하기 위한 함수들 Setter,Getter는 빼고 설명하겠습니다. 

Update - 대부분의 로직은 State에 위임. 기본 동작은 이동 -> 상태 유지체크 -> 다음 이동 타일 확인입니다. 자세한 건 CharacterState에서...

EnterFacility - 건물 들어갈시 동작

UseFacilityInTime - 건물 사용 함수 

ExitFacility - 건물 나갈시 동작

FindDestination - 각 캐릭터별로 빈 시설이 있는지 확인하여 리턴하는 함수.

 

 

public class TouristCharacter : Character {     public override Tile FindDestination()     {                  List<SellFacility> builds = facilityController.getSellFacility();         if (!isEmptyMoney(100))         {             return tileController.getTile(0, 0);         }         foreach (Facility build in builds)         {             if (build == beforeFacility)             {                 continue;             }             if (build.isEmpty())                 return build.getPositionTile();         }         return null;     } } public class TownCharacter : Character {      Facility homeFacility;      public void setHomeFacility(Facility _home)     {         homeFacility = _home;     }     public override Tile FindDestination()     {         List<GymFacility> builds = facilityController.getGymFacility(); ;         if (getHp() < 10)         {             return homeFacility.getPositionTile();         }         foreach (Facility build in builds)         {             if (build == beforeFacility)             {                 continue;             }             if (build.isEmpty())                 return build.getPositionTile();         }         return null;     } } 

 

상단이 관광객, 하단이 마을 주민입니다. 각 캐릭터들은 FindDestination을 상속받아 구현하고 있고, 관광객은 판매점을 빈 곳이 있는지 탐색하고, 돈이 부족하면 되돌아갑니다. 마을 주민은 운동시설을 빈 곳이 있는지 탐색하고, 체력이 부족하면 집으로 돌아갑니다. 단, 이전에 사용한 건물은 다른 건물을 사용하긴 전까지 사용하지 않습니다. 안그러면 혼자 독점할 것 같아서요.

 

 

CharacterState

 

public abstract class CharacterState {      static public CharacterStateIdle stateIdle = new CharacterStateIdle();     static public CharacterStateJob stateJob = new CharacterStateJob();     static public CharacterStateMove stateMove = new CharacterStateMove();      protected bool isRangeOut(int x, int y)     {         return (x < 0) || (y < 0) || (x >= Constants.Width) || (y >= Constants.Height);     }     public abstract bool Move(Character _character,Tile dest);     public abstract Tile FindNext(Character _character, TileController tileController);     public abstract CharacterState CheckTile(Character _character); }

 

추상 클래스는 위와 같습니다. 어차피 구현될 상태 클래스들은 함수만을 가지고 매개변수로 전달된 character 클래스의 필드를 이용해 로직이 진행될 것입니다. 그래서 static으로 각 state 인스턴스를 미리 만들어두고 모든 캐릭터가 공용으로 쓸 예정입니다.

Move - dest 타일로 이동하는 함수입니다. ( 이동은 한 칸씩 합니다.)

FindNext - 다음에 이동할 dest 타일을 찾는 함수입니다.

CheckTile - State를 변경해야할지 체크할 함수입니다.

 

public class CharacterStateIdle : CharacterState {     public override bool Move(Character _character, Tile dest)     {         if (dest == null)         {             return true;         }         Transform trans = _character.transform;         Vector3 desPos = dest.GetTileObject().transform.position + new Vector3(0, 0, -1.5f);         Vector3 direct = desPos - trans.position;          trans.Translate(direct.normalized * 0.4f *Time.deltaTime);         float diffPos = Vector3.Distance(desPos, trans.position);          if (diffPos > 0.1f)             return false;         return true;     }      public override Tile FindNext(Character _character, TileController tileController)     {         IntVector2[] checkPos = new IntVector2[4]            {             new IntVector2(0,1),new IntVector2(1,0),new IntVector2(0,-1),new IntVector2(-1,0)            };         int startIndex = Random.Range(0, 4);         Tile currentTile = _character.getCurrentTile();          int checkX = currentTile.GetPosX() + checkPos[startIndex].x;         int checkY = currentTile.GetPosY() + checkPos[startIndex].y;          for (int i = 0; i < 4; i++)         {             int index = (startIndex + i) % 4;             int tempX = currentTile.GetPosX() + checkPos[index].x;             int tempY = currentTile.GetPosY() + checkPos[index].y;              if (isRangeOut(tempX,tempY))             {                 continue;             }             if (tileController.getTile(tempX, tempY).getFacility().isNull())             {                 _character.anim.SetFloat("PosX", checkPos[index].x);                 _character.anim.SetFloat("PosY", checkPos[index].y);                 return tileController.getTile(tempX, tempY);             }              checkX = tempX;             checkY = tempY;                      }          //사방이 건물로 막혔을 경우엔 마지막 체크지점을 반환.         return tileController.getTile(checkX, checkY);     }      public override CharacterState CheckTile(Character _character)     {         Tile dest = _character.FindDestination();         if(dest != null)         {             _character.setDestTile(dest);             return CharacterState.stateMove;         }          return CharacterState.stateIdle;     } }

 

서성거리는 상태인 Idle입니다. 길을 랜덤한 방향으로 이동하면서, CheckTile을 통해서 빈 시설을 찾으면 Move 상태로 변경됩니다.

 

public class CharacterStateMove : CharacterState {     public override bool Move(Character _character, Tile dest)     {         Transform trans = _character.transform;         Vector3 desPos = dest.GetTileObject().transform.position + new Vector3(0, 0, -1.5f);         Vector3 direct = desPos - trans.position;                  trans.Translate(direct.normalized * 0.4f * Time.deltaTime);         float diffPos = Vector3.Distance(desPos, trans.position);                  if (diffPos > 0.1f)             return false;         if(dest == _character.getDestTile())         {             if (!dest.getFacility().isNull())             {                 if (dest.getFacility().isEmpty())                 {                     _character.EnterFacility(dest.getFacility());                 }             }          }         return true;     }     public override Tile FindNext(Character _character, TileController tileController)     {         IntVector2[] checkPos = new IntVector2[4]            {             new IntVector2(0,1),new IntVector2(1,0),new IntVector2(0,-1),new IntVector2(-1,0)            };          Tile currentTile = _character.getCurrentTile();         Tile destTile = _character.getDestTile();         int value = destTile.getFacility().GetPathValue(currentTile.GetPosX(), currentTile.GetPosY());                  int checkX = currentTile.GetPosX() + checkPos[0].x;         int checkY = currentTile.GetPosY() + checkPos[0].y;          for (int i = 0; i < 4; i++)         {             int tempX = currentTile.GetPosX() + checkPos[i].x;             int tempY = currentTile.GetPosY() + checkPos[i].y;              if (isRangeOut(tempX,tempY))             {                 continue;             }             if(destTile.getFacility().GetPathValue(tempX, tempY) == value-1)             {                  _character.anim.SetFloat("PosX", checkPos[i].x);                 _character.anim.SetFloat("PosY", checkPos[i].y);                 return tileController.getTile(tempX, tempY);             }              //그 어디에도 v-1구간이 없다면 가장 작은 값을 가진 타일로 이동한다.             if (destTile.getFacility().GetPathValue(tempX, tempY) < destTile.getFacility().GetPathValue(checkX, checkY))             {                 checkX = tempX;                 checkY = tempY;             }         }         return tileController.getTile(checkX, checkY);     }     public override CharacterState CheckTile(Character _character)     {         Tile dest = _character.getDestTile();         if (dest.getFacility().isEmpty() && !dest.getFacility().isNull())         {             return CharacterState.stateMove;         }         return CharacterState.stateIdle;     } }

 

목적지로 이동하는 Move상태입니다. 시설이 가지고 있는 로드맵을 따라 작은 값에 수렴하도록 이동합니다.  그리고 목적지에 도착한다면 시설에 들어갑니다. 목적지 시설이 파괴되거나 타인이 먼저 사용을하게 되면 다시 Idle 상태로 돌아갑니다.

 

public class CharacterStateJob : CharacterState {     public override bool Move(Character _character, Tile dest)     {         return false;     }     public override Tile FindNext(Character _character, TileController tileController)     {          return null;     }     public override CharacterState CheckTile(Character _character)     {          return CharacterState.stateJob;     } }

 

Job은 실제 건물에 들어간 상태인데....이건 뭐 Null이랑 다를바가 없습니다. 캐릭터의 능력치를 업하는 것은 건물이 캐릭터에게 부여해주는 형식이기때문에 별다른 행동은 하지 않고있습니다.

 

 크게는 시설쪽에서 로드맵 구현이 있는데... 이 부분은 향후에 기능에 살을 붙여나가면서 다시 포스팅이 될 것 같으니 그때 다시 하도록 하겠습니다.

 

이제... 다음엔 DB 연동을 통해 시설물을 좀 더 풍부하게 해볼 예정입니다. (전 sqlite를 사용할 예정입니다.) 그 이후부터는 기본 기능에 대해 살을 붙여나가면서 리펙토리을 같이 겸하는 포스팅이 될 것 같습니다.

 

 

현재까지 진행된 git url ( Unity 2017.4.1f1)

 - https://github.com/zprooo915/box

 

Posted by 검은거북

이번 포스팅은 좀 간단할 듯 하네요.


건물 버프 기능과 캐릭터와의 상호작용 부분입니다.

내용 자체는 기획에 따라 변경될거라 별거 없지만, Facility에 대해 짚고 넘어가고자 진행합니다. 


아래와 같은 것을 만드는 겁니다. 



(화면상에 눈에 보이는게 없어서 급조해서 만든 Text와 머리만 있는 캐릭터가 참... 없어보이네요. )

 현재는 캐릭터가 지정된 타일로 이동해서 타일의 시설을 사용하고 되돌아오는 테스트 코드만 작성된 상태입니다. (캐릭터는 길찾기 및 사용할 시설 선택 로직까지 구현하고, 아마 다음 포스팅에 올리겠습니다. 캐릭터 몸통과 애니메이션도 그때 같이 진행할 예정입니다.)


이번에는 간단하게 Facility를 어떻게 구조를 잡았고, 어떤 것을 하는지만 얘기하겠습니다.

딱 기본이 될 정도만 하다보니 빈 함수도 많고, 구현내용이 많지가 않아요. (기획에 따라 계층구조를 다시 생각해 볼 필요도 있겠네요...)


우선 Facility


public abstract class Facility { public FacilityType type; public Sprite facilitySprite; protected GameObject facilityObj; float useTime = 4.0f; int useAnimation = 0; protected Tile positionTile; public float getUseTime() { return useTime; } public int getUseAnimaion() { return useAnimation; } public void CreateFacility(Tile tile) { GameObject obj = new GameObject("facility"); obj.transform.SetParent(tile.GetTileObject().transform); obj.AddComponent<SpriteRenderer>().sprite = facilitySprite; obj.transform.localPosition = new Vector3(0, 0.5f, -1); facilityObj = obj; positionTile = tile; MakeFacility(); } public abstract bool isEmpty(); public abstract bool isNull(); public abstract void ApplyBuff(Dictionary<BuffType, int> buffList); public abstract void MakeFacility(); public abstract bool InteractWithCharacter(Character character); public abstract void ExitFacility(); }


각 함수와 필드에 대해 간단히 설명하겠습니다.


useTime - 캐릭터가 들어와서 사용시 사용할 시간.

useAnimation - 캐릭터가 사용시 플레이될 애니메이션 번호 ( 애니메이션은 캐릭터가 개별적으로 가지고, 건물 사용시 마크된 애니메이션을 플레이 시킬 예정입니다.)


isEmpty - 향후 캐릭터가 이동할 건물을 검색할 시 사용될 건물이 비었는지 판별 함수.

isNull - NullFacility 구별용

ApplyBuff - 매개변수를 통해 전달된 버프를 이용해 시설의 데이터를 변경하는 함수. (버프는 타일이 가지고 있다가 건물이 설치되면 건물에 전달.)

MakeFacility - 건물 설치시 하위 클래스가 해야하는게 있으면 할 함수.

InteractWithCharacter - 건물을 사용하는 캐릭터와 상호작용 

ExitFacility - 캐릭터가 나갈시 호출 함수.



그리고 Facility를 상속받아 NullFacility, EtcFacility, SellFacility, GymFacility, DecoFacility가 만들어집니다.

(NullFacility는 Null체크 회피를 위한 클래스일 뿐이라 제외하겠습니다.)


판매점 클래스인 SellFacility 입니다.

public class SellFacility : Facility { public int fame = 0; public int originPrice = 100; public int price; Character user; string textPoolName = "TextPool"; public SellFacility(FacilityType _type) { type = _type; facilitySprite = SpriteManager.Instance.getFacilitySprite(_type); price = originPrice; } public override bool isNull() { return false; } public override void ApplyBuff(Dictionary<BuffType, int> buffList) { if (buffList.ContainsKey(BuffType.Envir)) { fame = buffList[BuffType.Envir]; ShowMoveText("Fame " + buffList[BuffType.Envir]); } if (buffList.ContainsKey(BuffType.Price)) { price = originPrice + buffList[BuffType.Price]; ShowMoveText("Price " + buffList[BuffType.Price]); } Debug.Log("fame = " + fame + " price = " + price); } private void ShowMoveText(string _text) { MoveText textObj = PoolManager.Instance.getObject(textPoolName, facilityObj.transform).GetComponent<MoveText>(); textObj.setTextMessage(_text); textObj.transform.position = facilityObj.transform.position + new Vector3(0, 0, -1); textObj.gameObject.SetActive(true); } public override void MakeFacility() { } public override bool InteractWithCharacter(Character character) { user = character; if (user.isEmptyMoney(price)) { user.addMoney(-1 * price); } return false; } public override bool isEmpty() { return user == null; } public override void ExitFacility() { user = null; } }


아래의 GymFacility와 비슷해서 밑에서 같이 설명하겠습니다. 


체육관 유형 클래스인 GymFacility

public enum AbilityType
{
    Str,Spd
}

public class GymFacility : Facility
{
    public int fame = 0;
    AbilityType ability = AbilityType.Str;
    int abiliValue = 20;

    Character user;

    string textPoolName = "TextPool";
    public GymFacility(FacilityType _type)
    {
        type = _type;
        facilitySprite = SpriteManager.Instance.getFacilitySprite(_type);

    }
    public override bool isNull()
    {
        return false;
    }
    public override void ApplyBuff(Dictionary<BuffType, int> buffList)
    {
        if (buffList.ContainsKey(BuffType.Envir))
        {
            fame = buffList[BuffType.Envir];
            ShowMoveText("Fame " + buffList[BuffType.Envir]);
        }
        Debug.Log("fame = " + fame);

    }
    private void ShowMoveText(string _text)
    {

        MoveText textObj = PoolManager.Instance.getObject(textPoolName, facilityObj.transform).GetComponent<MoveText>();
        textObj.setTextMessage(_text);
        textObj.transform.position = facilityObj.transform.position + new Vector3(0, 0, -1);
        textObj.gameObject.SetActive(true);
    }
    public override bool InteractWithCharacter(Character character)
    {
        user = character;
        if(ability == AbilityType.Str)
        {
            user.addStr(abiliValue);
        }
        else if(ability == AbilityType.Spd)
        {
            user.addSpd(abiliValue);
        }
        return false;

    }

    public override void MakeFacility()
    {
    }

    public override bool isEmpty()
    {
        return user==null;
    }
    public override void ExitFacility()
    {
        user = null;
    }
}


ShowMoveText 함수는 화면상에 변경되는 데이터양을 표현하기 위해 급조한 함수입니다;;

그 외에 Gym과 Sell에서 구현된 함수는 ApplyBuff와 InteractWithCharacter입니다.

전달된 매개변수에 따라 데이터에 적용하고, InteractWithCharacter 는 캐릭터가 건물사용시 호출되는 함수로 건물별로 캐릭터에 데이터를 적용하고 있습니다.

MakeFacility는 아직 구현이 안되있지만 향후 캐릭터가 목표 건물을 탐색하기위해 리스트에 Add하는 코드를 구현할 예정입니다.


조형물 유형 클래스인 DecoFacility

public class DecoFacility : Facility
{
    int range;
    BuffType buffType;
    int buffValue;

    TileController tileController;

    public DecoFacility(FacilityType _type,int _range,BuffType _buffType, int _buffValue)
    {
        type = _type;
        facilitySprite = SpriteManager.Instance.getFacilitySprite(_type);

        tileController = GameObject.Find("TileController").GetComponent<TileController>();
        
        range = _range;
        buffType = _buffType;
        buffValue = _buffValue;
    }
    public override bool isNull()
    {
        return false;
    }
    public override void ApplyBuff(Dictionary<BuffType, int> buffList)
    {

    }
    public override bool InteractWithCharacter(Character character)
    {

        return false;
    }

    public override void MakeFacility()
    {
        int posX = positionTile.GetPosX();
        int posY = positionTile.GetPosY();

        tileController.SetTileBuff(range, posX, posY, buffType, buffValue);
    }
    public override bool isEmpty()
    {
        return false;
    }
    public override void ExitFacility()
    {
    }
}
TileController  
  public void SetTileBuff(int range, int posX, int posY, BuffType type, int value)
    {
        for(int i = -range; i <= range; i++)
        {
            for(int j = -range; j <= range; j++)
            {
                if (i == 0 && j == 0) continue;

                int _x = posX + j;
                int _y = posY + i;
                if (isRangeOut(_y, _x)) continue;

                tiles[_y, _x].AddBuffList(type, value);
            }
        }

    }


 조형물은 주요 역할이 주변 건물에 버프를 주는 것입니다. 때문에 주변 타일에 자신의 버프를 적용하고 있습니다.

 버프는 우선 타일에 적용되고, 타일은 버프가 변경될 때마다 Facility에 적용하는 함수를 호출해줍니다.


그 외 Facility ( 현재는 캐릭터의 집을 나타냅니다.)

public class EtcFacility : Facility
{
    Character master;

    bool isUse = false;
    int recovery = 10;

    public EtcFacility(FacilityType _type)
    {
        type = _type;
        facilitySprite = SpriteManager.Instance.getFacilitySprite(_type);

    }
    public override bool isNull()
    {
        return false;
    }

    public override void ApplyBuff(Dictionary<BuffType, int> buffList)
    {

    }
    public override bool InteractWithCharacter(Character character)
    {

        character.addHp(recovery);

        return !character.isFullHp();

    }
    
    public override void MakeFacility()
    {
    }

    public override bool isEmpty()
    {
        return isUse;
    }
    public override void ExitFacility()
    {
        
    }
}



음....쓰고나니 더 별 내용이 없네요...간단한데도 불구하고 포스팅이 늦었는데...흠... 앞으로 더 늦어질수도 있습니다;;;

안드로이드 쪽을 너무 공부를 안하고 있었어서, 최근에 다시 들여다보면서 안드로이드 네이티브 앱도 같이 개발을 좀 연습하다보니...


이번에는 캐릭터 클래스는 설명이 없는데, 다음 포스팅에서는 캐릭터 건물 탐색 및 길찾기로 캐릭터 구현을 진행하게 될테니 같이 설명하겠습니다.

Posted by 검은거북

세번째, 메뉴를 통해서 건물 설치하는 부분까지.


아래 같은 것을 만드는 겁니다.




원래는 메뉴 / 건물 설치로 하려고 했는데, 메뉴는 별 내용이 없어서... 건물 설치 파트와 이전 버전에서 변경된 지점만 진행을 하겠습니다. 


우선 이전의 InputController와 State 클래스입니다.

이전 포스팅의 InputController를 일반 / 설치 / 메뉴 상태로 나누어 변경된 지점입니다.

public class InputController : MonoBehaviour {
    
    Vector3 lastMousePos;

    TileController tileController;
    FacilityController facilityController;

    Dictionary<statetype,state> stateMap = new Dictionary<statetype, state>();

    State state;
	// Use this for initialization
	void Start () {

        tileController = GameObject.Find("TileController").GetComponent<tilecontroller>();
        facilityController = GameObject.Find("FacilityController").GetComponent<facilitycontroller>();
        CreateState();
        
        SetState(StateType.Normal);
    }
	void CreateState()
    {
        stateMap.Add(StateType.Normal, new NormalState(tileController));
        stateMap.Add(StateType.Install, new InstallState(tileController,facilityController, this));
        stateMap.Add(StateType.Menu, new MenuState());
    }

	// Update is called once per frame
	void Update () {
        if (Input.GetMouseButton(1))
        {
            state.UpdateDrag(lastMousePos);
        }

        lastMousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        lastMousePos.z = 0;

        state.UpdateMove(lastMousePos);

        if (Input.GetMouseButtonDown(0))
        {
            state.UpdateClick();
        }
    }
    public void SetState(StateType _type)
    {
        if (state != null)
        {
            state.ExitState();
        }
        state = stateMap[_type];
        state.EnterState();
    }
}

State는 각각 미리 객체를 생성해두어 Dictionary로 객체 풀 관리를 합니다. (CreateState)

호출시에는 SetState를 통해 state 변경시 바꾸기 직전, 직후 해야할 일을 진행하도록 하였읍니다.

그리고 Update에 있던 기존의 코드는 모두 NormalState로 옮겨 state에 위임하도록 변경하였습니다. 솔직히 Drag,Click,Move 다 하나로 만들어도 될거 같긴한데.... 역할별로 작성하는게 더 용이하게 느껴지더군요.

(InputController는 가능하면 앞으로 건들지 않았으면 좋겠네요.)


다음은 State와 그 하위클래스들.

 아래 하위 클래스 외에 Null처리를 해줄 NullState와 메뉴상태의 MenuState가 있는데 별도로 언급할 내용이 없습니다.

public enum StateType
{
    Normal,Install,Menu
}

public abstract class State  {

    public abstract void EnterState();
    public abstract void ExitState();
    public abstract void UpdateMove(Vector3 _pos);
    public abstract void UpdateClick();
    public abstract void UpdateDrag(Vector3 _pos);
    
    protected Vector3 getTileCenterPosFromMouse(Vector3 _mousePos)
    {
        _mousePos.y = _mousePos.y * 2;
        int mPosX = Mathf.FloorToInt(_mousePos.x);
        int mPosY = Mathf.FloorToInt(_mousePos.y);

        // 홀짝 구분 / 노출되는 타일들의 중앙점은 합이 짝수다.
        int checkEven = (mPosX + mPosY) & 1;
        // 홀수라면 remainX에 곱하여 양수로 바꿔준다.
        int tempEven = (checkEven * -2) + 1;

        // 홀수라면 짝수로 기준센터 이동.
        mPosX += checkEven;

        float remainX = _mousePos.x - mPosX;
        float remainY = _mousePos.y - mPosY;

        // 소수점 이하의 수를 더하여, 1보다 크면 이동.
        float remainSum = (tempEven * remainX) + remainY;
        // 더한 값을 내림하여, 1.0 이상이면1로 만들어 최종 계산식에 사용.
        int floorSum = Mathf.FloorToInt(remainSum);

        // 더한 값이 1.0 이상이고, checkEven이 짝수라면 x+1,y+1  (floorSum = 1 , tempEven = 1)
        // 더한 값이 1.0 이상이고, checkEven이 홀수라면 x-1,y+1  (floorSum = 1 , tempEven = -1)
        // 더한 값이 1.0 이하라면, x,y  (floorSum = 0)
        Vector3 result = new Vector3(mPosX + (floorSum * tempEven), (mPosY + floorSum) * 0.5f, -1);

        //Debug.Log("_mousePos = " +_mousePos + " mPosX = " + mPosX + " mPosY = " + mPosY + " checkEven = " + checkEven + " tempEven = " + tempEven + " remainX = " + remainX + " remainY = " + remainY + " remainSum = " + remainSum +" FloorSum = " + floorSum + " result = " + result);

        return result;
    }
}


추상클래스인 State는 

State변경 직후 필요한 부분을 작성하는 EnterState

State변경 직전 필요한 부분을 작성하는 ExitState

마우스 움직임에 대응하는 UpdateMove

마우스 왼쪽 클릭에 대응하는 UpdateClick

마우스 오른쪽 드래그에 대응하는 UpdateDrag

그리고 이전에 작성한 그대로인 getTileCenterPosFromMouse로 이루어져있습니다.


public class NormalState : State
{
    TileController tileController;
    
    GameObject mousePointer;

    public NormalState(TileController _tile)
    {
        tileController = _tile;
        mousePointer = new GameObject("MousePointer");

        mousePointer.AddComponent<SpriteRenderer>().sprite = SpriteManager.Instance.getFacilitySprite(FacilityType.Empty);

        Color facilColor = mousePointer.GetComponent<SpriteRenderer>().color;
        facilColor.a = 1.0f;
        mousePointer.GetComponent<SpriteRenderer>().color = facilColor;

    }
    public override void UpdateClick()
    {
        Vector3 _pos = mousePointer.transform.position;
        int _posX = Mathf.FloorToInt(_pos.x);
        int _posY = Mathf.FloorToInt(_pos.y*2);

        int _x = (_posY + _posX) >> 1;
        int _y = (_posY - _posX) >> 1;

        tileController.getTile(_x, _y).GetInformation();
    }

    public override void UpdateDrag(Vector3 _pos)
    {
        Vector3 currentPos = currentPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        currentPos.z = 0;

        Camera.main.transform.Translate(_pos - currentPos);
    }

    public override void UpdateMove(Vector3 _pos)
    {
        mousePointer.transform.position = getTileCenterPosFromMouse(_pos);
    }

    public override void EnterState()
    {
        mousePointer.SetActive(true);
    }

    public override void ExitState()
    {
        mousePointer.SetActive(false);
    }
}

public class InstallState : State
{
    Facility selectFacility;
    TileController tileController;
    FacilityController facilityController;
    InputController input;
    GameObject mousePointer;

    public InstallState(TileController _tile, FacilityController _facilityController, InputController _inputController)
    {
        tileController = _tile;
        selectFacility = new NullFacility();
        facilityController = _facilityController;
        input = _inputController;

        mousePointer = new GameObject("buildPointer");
        mousePointer.AddComponent<SpriteRenderer>().sprite = selectFacility.facilitySprite;

        Color facilColor = mousePointer.GetComponent<SpriteRenderer>().color;
        facilColor.a = 0.5f;
        mousePointer.GetComponent<SpriteRenderer>().color = facilColor;    
    }
    public void SetSelectFacility(Facility _selectFacility)
    {
        selectFacility = _selectFacility;
        mousePointer.GetComponent<SpriteRenderer>().sprite = _selectFacility.facilitySprite;
    }
    public override void UpdateClick()
    {

        Vector3 _pos = mousePointer.transform.position;
        int _posX = Mathf.FloorToInt(_pos.x);
        int _posY = Mathf.FloorToInt(_pos.y * 2);

        int _x = (_posY + _posX) >> 1;
        int _y = (_posY - _posX) >> 1;

        bool isInstall = tileController.getTile(_x, _y).ChangeFacility(selectFacility);

        if (isInstall)
        {
            input.SetState(StateType.Normal);
        }
    }

    public override void UpdateDrag(Vector3 _pos)
    {
        Vector3 currentPos = currentPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        currentPos.z = 0;

        Camera.main.transform.Translate(_pos - currentPos);
    }

    public override void UpdateMove(Vector3 _pos)
    {
        Vector3 movePos = getTileCenterPosFromMouse(_pos);
        movePos.y += 0.5f;
        mousePointer.transform.position = movePos;
    }
    public override void EnterState()
    {
        SetSelectFacility(facilityController.getSelectFacility());
        mousePointer.SetActive(true);
    }

    public override void ExitState()
    {
        mousePointer.SetActive(false);
    }

}


위에가 Normal상태, 아래가 Install 상태로,

각각 자신이 사용할 포인터( 건물모양, 하이라이트)를 생성하고, EnterState에서 활성화를 해주고, ExitState에서 비활성화를 해주고 있습니다.


 Normal은 이전 포스팅에서 작성한걸 역할별로 나누어 작성되어있을 뿐입니다.

 Install은 EnterState에서 FacilityController로부터 선택된 Facility를 받아오고, 마우스 포인터를 해당 건물의 모양으로 변경해주고 있습니다. (FacilityController는 설치할 건물 선택시 selectFacility로 선택된 건물을 저장하고 있습니다.)

그리고 빈 타일에 클릭시 타일에 선택된 건물을 생성해주고, Normal상태로 변경합니다.

(옮기고나니 중복 코드 구린내가 좀 보이는 것 같네요....흠..)


다음은 Facility와 FacilityController.

public abstract class Facility  {
    
    public FacilityType type;
    public Sprite facilitySprite;
    GameObject facilityObj;

    public void CreateFacility(GameObject tileObj)
    {
        GameObject obj = new GameObject("facility");
        obj.transform.SetParent(tileObj.transform);
        obj.AddComponent<SpriteRenderer>().sprite = facilitySprite;
        obj.transform.localPosition = new Vector3(0, 0.5f, -1);

        InteractWithFacility();
    }
    public abstract bool isNull();
    public abstract void InteractWithFacility();
    public abstract void InteractWithCharacter();
}


public class FacilityController : MonoBehaviour {

    Facility selectFacility;
    InputController inputController;
    void Start()
    {
        inputController = GameObject.Find("InputController").GetComponent<InputController>();
    }
    public Facility getSelectFacility()
    {
        return selectFacility;
    }
    
    public void BuildButton(int _typeIndex)
    {
        //Test용

        // 향후 DB 인덱스로 변경하여 DB와 연동하여 건물 데이터 얻어오도록 변경. (Type까지 DB로 관리)
        FacilityType _type = (FacilityType)_typeIndex;

        Facility build;

        if (_type == FacilityType.Room1)
        {
            build = new EtcFacility(_type);
        }
        else if (_type == FacilityType.Seller)
        {
            build = new SellFacility(_type);

        }
        else if (_type == FacilityType.Flower)
        {
            build = new DecoFacility(_type);
        }
        else if (_type == FacilityType.Playground)
        {
            build = new GymFacility(_type);
        }
        else
        {
            build = new NullFacility();
        }
        selectFacility = build;
        inputController.SetState(StateType.Install);
    }
}


Facility의 하위클래스들은 다음에 건물 기능과 동작 추가를 하면서 같이 하고, 이번에는 뺏습니다. 설명도 다음 포스팅에 같이 하겠습니다. 이번에는 설치까지만 됐으니까요.


CreateFacility는 타일 오브젝트 하위에 Facility 객체를 생성하는 공통 함수로, InstallState에서 타일의 건물 교체를 호출하면 타일을 통해 호출되는 함수입니다.


FacilityController는 Facility  생성과 관리에 관여하는 클래스로, Test를 위해서 BuildButton을 만드는데 일단은 FacilityController에 옮겨놨습니다. 실제 메뉴에서 설치할 건물을 클릭하면 BuildButton이 호출됩니다. 향후에 DB가 연동된다면,(기능 구현 뒤에 예정) 변경할 예정입니다.


다음엔 건물 기능/ 동작 추가 하고, 캐릭터 프로토타입까지일듯 하네요.

Posted by 검은거북

앞으로는 단순화해서 포스팅하는 것보다 진행부분 별로 포스팅하도록 바꿀 생각입니다. 파트별로 진행을 해서 포스팅하면서도 좀 정리를 할 겸. 그래서 전략 게임 진행의 첫번째로 Isometric - 마름모 타일 구현하기!


아래 이미지 같은걸 만드는 겁니다. 참고하시길






위와 같은 타일을 isometic 이라고 많이 하더라고요.


솔직히 타일 만드는 것 자체는 쉬운데, 타일 위를 마우스가 지나갈 때 타일별로  하이라이트를 주는 부분에서 생각보다 오래 걸렸네요.


아주 단순하게 생각하면 타일 별로 콜라이더를 적용해서 판별하면 되는데, 이렇게 규칙적으로 생성된 타일은 분명 연산식으로 처리가능 할거고, 앞으로 캐릭터랑 건물들 연산량 쌓일거 생각하면... 반드시 연산식으로 바꿔야 겠다고 생각해서... 고민하느라 오래 걸렸네요.


우선 타일 생성 및 배열 컨트롤을 담당할 TileController

public class TileController : MonoBehaviour { Tile[,] tiles; public Sprite tileImage; // Use this for initialization void Start () { tiles = new Tile[32, 32]; for(int i = 0; i < 32; i++) { for (int j = 0; j < 32; j++) { // i = y , j = x GameObject tileObj = new GameObject("Tile_" + i + "_" + j); tileObj.transform.position = new Vector3((-1*i) +j, (i*0.5f) + (j*0.5f),0); tileObj.transform.SetParent(transform); tileObj.AddComponent<spriterenderer>().sprite = tileImage; tiles[i, j] = new Tile(i,j,tileObj); } } } }


public class Tile { int posX; int posY; Facility facility; GameObject tileObj; public Tile() { facility = new NullFacility(); } public Tile(int y,int x, GameObject _obj) { posX = x; posY = y; tileObj = _obj; facility = new NullFacility(); } public bool ChangeFacility(Facility _facility) { if (facility.isNull()) { _facility.CreateFacility(tileObj); facility = _facility; return true; } return false; } public void GetInformation() { Debug.Log("posX = " + posX + " posY = " + posY); } }

지금은 그냥 딱 타일 생성만 하는 부분입니다. 

로직 관련해서는 너무 단순해서 딱히 설명 할게 없네요.

타일에 관해서는 GameObject가 타일을 컴포넌트로 관리할지, 타일이 GameObject를 가져서 처리를 할지는 좀 더 생각해야겠습니다. (현재는 후자)



InputController - 마우스 컨트롤을 관리하는 클래스

public class InputController : MonoBehaviour { public Sprite mousePointerSprite; GameObject mousePointer; Vector3 lastMousePos; // Use this for initialization void Start () { mousePointer = new GameObject("MousePointer"); mousePointer.AddComponent<spriterenderer>().sprite = mousePointerSprite; } // Update is called once per frame void Update () { if (Input.GetMouseButton(1)) { Vector3 currentPos = currentPos = Camera.main.ScreenToWorldPoint(Input.mousePosition); currentPos.z = 0; Camera.main.transform.Translate(lastMousePos - currentPos); } lastMousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition); lastMousePos.z = 0; mousePointer.transform.position = getTileCenterPosFromMouse(lastMousePos); } private Vector3 getTileCenterPosFromMouse(Vector3 _mousePos) { _mousePos.y = _mousePos.y * 2; int mPosX = Mathf.FloorToInt(_mousePos.x); int mPosY = Mathf.FloorToInt(_mousePos.y); // 홀짝 구분 / 노출되는 타일들의 중앙점은 합이 짝수다. int checkEven = (mPosX + mPosY) & 1; // 홀수라면 remainX에 곱하여 양수로 바꿔준다. int tempEven = (checkEven * -2) + 1; // 홀수라면 짝수로 기준센터 이동. mPosX += checkEven; float remainX = _mousePos.x - mPosX; float remainY = _mousePos.y - mPosY; // 소수점 이하의 수를 더하여, 1보다 크면 이동. float remainSum = (tempEven *remainX) + remainY; // 더한 값을 내림하여, 1.0 이상이면1로 만들어 최종 계산식에 사용. int floorSum = Mathf.FloorToInt(remainSum); // 더한 값이 1.0 이상이고, checkEven이 짝수라면 x+1,y+1 (floorSum = 1 , tempEven = 1) // 더한 값이 1.0 이상이고, checkEven이 홀수라면 x-1,y+1 (floorSum = 1 , tempEven = -1) // 더한 값이 1.0 이하라면, x,y (floorSum = 0) Vector3 result = new Vector3(mPosX + (floorSum * tempEven),(mPosY + floorSum)*0.5f,-1); //Debug.Log("_mousePos = " +_mousePos + " mPosX = " + mPosX + " mPosY = " + mPosY + " checkEven = " + checkEven + " tempEven = " + tempEven + " remainX = " + remainX + " remainY = " + remainY + " remainSum = " + remainSum +" FloorSum = " + floorSum + " result = " + result); return result; } }

 Update에서는 마우스 오른쪽 클릭을 하고, 마우스를 이동시 이전 프레임의 좌표와 현재좌표를 연산하여 카메라를 이동시켜 드래그하여 이동하는 연출을 하고 있습니다.

 Update는 나중에 상태 패턴을 이용해 평상시와 건물 설치시로 나눌 예정입니다.


그리고 위에서 말한 타일별 하이라이트를 주는 연산식이 getTileCenterPosFromMouse 입니다.

복잡한 연산할 때는 주석을 미리 써놓고 코딩해서 주석에서 설명을 거의 하고는 있지만, 조금 부연 설명을 하자면... (원래는 각 칸의 높이는 0.5지만, 편의상 1로 하겠습니다.)


 목적은 내 하이라이트 타일의 중심점을 각 타일의 중심점에 맞춰주는 것. (절대 (0,1)과 같은 타일의 중간값에 하이라이트가 위치해선 안됨)

 

 사진을 보시면, 모든 마름모 타일의 중앙은 x+y가 짝수이고, 모든 사각형 공간 안의 타일은 그림의 1번과 2번의 두 형태만 존재하는 것에 착안해서 만들었습니다.

그리고 마우스 좌표는 내림을 하면 좌하에 가장 가까운 좌표를 얻을 수 있죠.

  1. 마우스 좌표를 내림하여 기준 센터를 짝수로 통일 
    • 마우스 좌표의 내림 합이 짝수면, 2번 영역에 마우스가 있다는 것.
    • 마우스 좌표의 내림 합이 홀수면, 1번 영역에 마우스가 있다는 것. (x+1을 하여 짝수로)
      • 예를 들면, 내림이 (-1,0)이면 (0,0)을 기준으로 1번 영역에 있는 것이므로 (0,0)으로 기준 변경.
  2. 각 영역에 맞게 연산하고, 하이라이트 센터 값에 적용.
    • 1번은 (0,0)을 기준으로 x,y 소수점 절대값 합이 1.0이상이면 x-1, y+1을 해주고,
    • 2번은 (0,0)을 기준으로 x,y 소수점 절대값 합이 1.0이상이면 x+1, y+1을 해줍니다.
    • 공통적으로 절대값의 합이 1.0이하면 x,y


isometric 연산식들을 찾아도 봤는데, 위에 말한 단순한 콜라이더 방식도 나오고, 다른 연산은 if-else 문을 이용해서 길게 하는 것도 있더라고요. 마음에 드는 걸 못 찾아서 그냥 만들었습니다. (좀 더 수학공식이나 그래프에 빠삭하면 하는 아쉬움이...) 혹시 비슷한걸 만드시려는 분이시려면 더 좋은 로직을 만드시길... 아마 좀 더 찾으면 대단하신 분들이 간단하게 한게 있을거 같긴한데....


다음에는 건물 설치/ 상태 패턴 적용 을 하겠습니다.

Posted by 검은거북


사진은 카이로소프트의 던전 마을 스토리입니다.


요런거를 만들려고 생각합니다. 

여러 칸으로 이루어진 필드에 오브젝트를 설치하고, AI를 가진 캐릭터가 이용하여 재화 또는 능력치를 올린다. 이게 핵심이겠네요. 


기획

1. 32 X 32 필드

2. 설치가능한 시설은 조형물 / 판매점 / 운동시설 / 거주시설로 이루어진다.

  1) 조형물 - 주변 시설의 효과 / 이용률을 증가 시킨다.

  2) 판매점 / 오락시설 - 캐릭터가 이용하면 마을의 재화가 증가한다.

  3) 운동시설 - 캐릭터 또는 마을의 능력치가 올라간다.

  4) 거주시설 - 마을 주민을 증가시킨다.

3. 캐릭터는 마을 주민/ 관광객으로 이루어진다.

   1) 마을 주민은 운동시설 위주로 사용한다.

   2) 관광객은 판매점 위주로 사용한다.

4. 캐릭터는 체력, 재화, 힘, 스피드를 가진다.

5. 캐릭터는 마을 전체를 이동가능하다.

6. 시설은 주변이 막히면 진입이 불가하다.

6. 마을은 재화 / 명성/ 마을 포인트를 가진다.


플로우 

1. 필드 - 최초 시작시 32 X 32의 타일을 차례로 생성.

     - 타일 타입을 배열로 관리.

     - 배열 생성 및 배열 변화 관리.

2. 타일

   - 타일은 이동 가능/ 불가능 (시설 설치) 타입으로 구분

      - 이동 불가능은 시설물 타입 번호로 관리.  시설물 없음/ 시설물 번호로 통일.

      - 타일 정보 - 타일에 설치된 건물 인스턴스 (클릭시 정보 출력) / 길찾기 정보

3. 시설물

   - 조형물 / 오락시설/ 운동시설/ 거주시설 로 구분.

     - 설치시 필드 배열에 해당 건물 반영.  / 

     - 객체는 같은 건물을 공유 ( 프로토타입 clone)

       - 공통 - 설치 기능 / 위치 정보/ 타입/  

       - 조형물 - 효과 타입/ 효과 수치 / 범위   (옵져버/ 콜백으로 효과 적용)

       - 오락시설 / 판매점 - 가격 / 선호도

       - 운동시설 - 상승 타입 (명성/ 마을포인트) / 선호도

       - 거주시설 - 거주 캐릭터

     - 컨트롤러에서 시설물 관리.

4. 캐릭터

   - 유형과 선호도/ 랜덤 함수에 의해 목표 건물을 결정하고, 길을 따라 최단 경로로 이동.

   - (캐릭터가 많을 것이라서 길찾기 알고리즘의 연산량에 FPS 영향이 클 듯.)

   - 길찾기 - 

5. 인터렉티브 (상태)

   - 일반 - 이동시 초점 강조 / 클릭시 해당하는 객체 정보출력 / 드래그시 이동에 따라 화면 이동

   - 설치 - 이동시 설치 건물 이동 / 클릭시 해당 위치에 객체 설치 및 반영 / 드래그 - X

   - 선택창 - 이동 / 드래그 X , 클릭시 설치할 시설 선택 - 설치 상태로 이동.

   

Posted by 검은거북

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

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

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


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

  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 검은거북

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

 때문에 기존에 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 검은거북
이전버튼 1 2 이전버튼

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

공지사항

Yesterday
Today
Total

달력

 « |  » 2024.5
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 31

최근에 올라온 글

최근에 달린 댓글

글 보관함