GameDev/Cubidom

Cubidom - AI 시스템 보완 (Utility AI)

SMNNMN 2026. 5. 3. 22:54

적 AI 시스템을 수정할 것이다.

기존 AI의 잘못된 설계와, 더 단단한 점수 체계를 만들어 줄 것이다.

 

새로운 AI설계 방향의 시작은 행동의 점수 기준으로, 

기존과 같이 크게 안전성(defence),지속 가능성(Resoruce), 공격성(Attack)으로 구분하였다.

먼저 기존의 문제점은 안전성이 높아도, 코어에 붙어있는 큐브의 개수가 적어서 코어를 막는 행동을 거의 하지 않는다는 것이였다.

또한 공격성의 문제로는 대포가 코어쪽을 바라보지 않는 문제가 가장컸다.

 

새로운 추가 점수 기준은 코어 주변의 큐브가 적을 경우, 안전성을 우선시하고, 그 이후 지속가능성이나, 공격성의 점수를 부여한다.

두번째로 대포를 설치했을 때, 공격할 대상이 존재하는가에 따른 점수를 부여한다.

각 행동점수와 위치 점수의 최고점을 100으로 잡았다. 위치점수는 거의 상관이 없지만 행동점수의 경우, 각 행동마다 점수 기준이 같아야 하기 때문에, 이렇게 각 행동의 기준들의 합을 정해주었다.

 

EnemyDefence

행동 점수

큐브 생성의 경우, 굉장히 빈번하게 생성될 수 있게, 기준을 후하게 잡았다. 이는 큐브의 생성이 많아짐으로써 다양한 형태를 볼수 있고, 또 여러 행동 갈래가 나올 수 있기 때문이다. 또한 게임이 너무 빨리 끝나지 않게, 코어의 안전성에 집중하였다.

적의 블럭 생성에 관한 행동점수 코드이다.

점수 기준은 자신의 코어와 가까이 있는 큐브의 수 (60점),  상대의 대포 개수(20점), 자신의 자원 생산자 수(20점)이다.

 

 public override float EvaluateAction()
 {
     // 자신의 큐브가 적을 때 점수증가
     // 자원량이 적을때 점수 감소, 많을 때, 점수증가
     // 상대 공격요소가 많을 때 증가,
     // 자원 구조물이 많을 때 증가, 적을 때 감소
     float score = 0;

     float nearCube = 0; // 코어와 가까이 있는 큐브 수
     // 코어 주변 블럭 개수
     foreach(var block in cubeCreator.blockList)
     {
         if(block is Cube && block.myCaster is EnemyAI && block is not Core) // 자신의 큐브들 (코어 제외)
         {

             if(Mathf.Abs(ai.core.worldPosition.x - block.worldPosition.x) <= 3)
             {
                 nearCube++;
             }
             else if (Mathf.Abs(ai.core.worldPosition.y - block.worldPosition.y) <= 3)
             {
                 nearCube++;
             }
             else if (Mathf.Abs(ai.core.worldPosition.z - block.worldPosition.z) <= 3)
             {
                 nearCube++;
             }

         }
     }
     // 0개일 때 60점, 12개일 때 0점
     // score += Mathf.Clamp(60 - (nearCube * 5), 0, 60);

     // 0개일 때 60점, 20개일 때 0점
     score += Mathf.Clamp((1 - (nearCube / 20f)) * 60, 0, 60);
     Debug.Log($"nearCube : {nearCube}");        

     float playerCannon = 0;
     foreach(var block in cubeCreator.blockList)
     {
         if(block is Cannon && block.myCaster is InputController) // 상대의 대포 개수
         {
             playerCannon++;
         }
     }
     // 10개 일때 10점, 0개일 때, 0점
     score += Mathf.Clamp(playerCannon * 2, 0, 20);
     Debug.Log($"playerCannon : {playerCannon}");

     // score += Mathf.Min(playerCannon * 10,50); //  상대 공격 요소가 만을 때 증가

     float myResource = 0;
     foreach(var block in ai.haveBlock)
     {
         if(block is ResourceProducer) // 내 자원 생산자 수
         {
             myResource++;
         }
     }
     // 6개면 30점, 0개면 0점
        score += Mathf.Clamp(myResource * (20 / 6f), 0, 20);
     Debug.Log($"myResource : {myResource}");

     // if(myResource <= 3) // 자신의 자원생산자의 수에 따른 점수 변동
     // {
     //     score -= 50 - myResource * 10;
     // }
     // else if(myResource >= 4 && myResource <= 10)
     // {
     //     score += myResource * 3;
     // }
     Debug.Log($"Defence Score : {score}");
     return Mathf.Clamp(score, 0, 100);
 }

처음 코어의 안전성을 확인하는 코드는 코어의 x,y,z를 확인하고 이와 근접한 worldPosition을 가지는 큐브의 개수를 센다.  이후 인접한 큐브의 수에 따라 점수를 부여한다. 0개일 때는 60점을 부여하며, 20개 일 때는 0점을 부여하는 식으로 Mathf.clamp 함수를 사용했다.

다음 기준으로 플레이어의 대포 개수를 확인해서 위협도를 사용하는 시스템을 구현했다.

마지막 기준은 자신의 자원생산자의 수를 확인하여 충분한 개수가 있으면 땅을 확장하는 시스템을 구현했다.

위치 점수 

큐브의 위치 점수 기준은 크게 3가지로 생성 위치가 코어와 떨어져있는 정도(50점), 상대 대포의 타겟이 될수 있는 정도(30점) , 자신의 대포에 따른 위치 점수(20점)로 나뉘어진다.

위치점수는 행동 점수와 다르게, 다른 스크립트와 점수를 맞출 필요가 없기 때문에, 각 기준들의 점수 비율만 잘 조절하면 된다.

public override float EvaluatePosition(Vector3 dir, Transform cube, Vector3 pos)
{
    // 코어 앞에 있을 경우 좋음
    // 자원 앞에 있을 경우 좋음
    // 공격 앞에 있을 경우 안좋음
    // 상대 공격 앞에 있을 경우 좋음

    Block block = cube.GetComponent<Block>();

    float score = 0;

    // 자신의 코어와 인접한가
    float nearValue = 0;

            nearValue += Mathf.Abs(ai.core.worldPosition.x - block.worldPosition.x);
        nearValue += Mathf.Abs(ai.core.worldPosition.y - block.worldPosition.y);
        nearValue += Mathf.Abs(ai.core.worldPosition.z - block.worldPosition.z);
        nearValue = Mathf.Abs(nearValue);

    // x,y,z 차의 합이 5이상 일시 0점,
    score += Mathf.Clamp(50 - (nearValue * 10), 0, 50);


    // 자신의 위치 기준 주변 대포의 거리
    float nearCannon = 0;
    float totalDistance = 0;

    foreach(var cannon in cubeCreator.blockList)
    {
        // 플레이어 대포 카운팅
        if(cannon is Cannon && cannon.myCaster is InputController)
        {
            // 모든 대포와의 거리 합산
            float dis = Mathf.Abs(block.worldPosition.x - cannon.worldPosition.x) +
                        Mathf.Abs(block.worldPosition.y - cannon.worldPosition.y) +
                        Mathf.Abs(block.worldPosition.z - cannon.worldPosition.z);
            // 멀리있는 대포 제외
            if(dis <= 20)
            {
                nearCannon++;
                totalDistance += dis;
            }
        }
    }
    // 모든 대포의 평균거리를 구해 점수로 환산
    if (nearCannon > 0)
    {
                    score += Mathf.Clamp((1 - (totalDistance / nearCannon) / 20f) * 30, 0, 30);

    }


    float cannonValue = 0;
    // 자신의 위치기준 자신의 대포
    foreach(var cannon in cubeCreator.blockList)
    {
        // 자신의 대포 검사
        if(cannon is Cannon && cannon.myCaster is EnemyAI)
        {
            Vector3 cannonDir = cannon.GetComponent<Cannon>().standardDir;

            if(cannonDir.x != 0)
            {
                if (cannon.worldPosition.y == block.worldPosition.y && cannon.worldPosition.z == block.worldPosition.z)
                {
                    if (cannonDir.x == 1)
                    {
                        if (cannon.worldPosition.x + 5 >= block.worldPosition.x && cannon.worldPosition.x < block.worldPosition.x)
                        {
                            cannonValue++;
                        }
                    }
                    else if (cannonDir.x == -1)
                    {
                        if (cannon.worldPosition.x - 5 <= block.worldPosition.x && cannon.worldPosition.x > block.worldPosition.x)
                        {
                            cannonValue++;
                        }
                    }
                }
            }
            else if(cannonDir.y != 0)
            {
                if (cannon.worldPosition.x == block.worldPosition.x && cannon.worldPosition.z == block.worldPosition.z)
                {
                    if (cannonDir.y == 1)
                    {
                        if (cannon.worldPosition.y + 5 >= block.worldPosition.y && cannon.worldPosition.y < block.worldPosition.y)
                        {
                            cannonValue++;
                        }
                    }
                    else if (cannonDir.y == -1)
                    {
                        if (cannon.worldPosition.y - 5 <= block.worldPosition.y && cannon.worldPosition.y > block.worldPosition.y)
                        {
                            cannonValue++;
                        }
                    }
                }
            }
            else if(cannonDir.z != 0)
            {
                if (cannon.worldPosition.x == block.worldPosition.x && cannon.worldPosition.y == block.worldPosition.y)
                {
                    if(cannonDir.z == 1)
                    {
                        if(cannon.worldPosition.z + 5 >= block.worldPosition.z && cannon.worldPosition.z < block.worldPosition.z)
                        {
                            cannonValue++;
                        }
                    }
                    else if(cannonDir.z == -1)
                    {
                        if (cannon.worldPosition.z - 5 <= block.worldPosition.z && cannon.worldPosition.z > block.worldPosition.z)
                        {
                            cannonValue++;
                        }
                    }
                }
            }
        }
    }

    score += Mathf.Clamp(20 - (cannonValue * 20), 0, 20);

    return score;
}

처음 기준으로 코어와 인접한가는 코어와의 x,y,z 거리를 확인하여 그 도합에 따라 점수를 부여한다.

두번째 기준으로 플레이어의 모든 대포를 검사하여, 생성 위치와의 거리 합을 모두 더한다. 여기서 임의로 먼 거리에 있는 대포는 생략한다. 이후, 모든 대포의 거리 평균을 내어 가까운 위치일수록 높은 점수를 부여한다.

세번째 기준으로 자신의 대포를 확인하여 해당 대포의 방향에 따라, 그 방향으로 5칸 이내에는 생성을 회피하게 한다. 뭔가 너무 코드를 일차원적으로 작성한 것 같은데, 나중에 벡터를 좀 공부해야 할 것 같다.

EnemyResource

행동 점수 

자원 생산의 경우, 초반에 많이 생성될 수 있게 코드를 구현했으며, 일정 개수 이후로는 거의 생성되지 않게끔 하였다. 이는 초반에 부족할 수 있는 자원량을 충당하고, 너무 많은 자원 생산자는 오히려 역효과를 낳기 때문이다.

자원 생산자를 생성하는 기준은 크게 3가지로,

자신의 자원량(40점), 자신의 자원생산자의 개수(30점),  자신의 큐브 개수(30점)로 기준을 잡았다.

  public override float EvaluateAction()
  {
      // 자원량이 적을 때 점수높음 , 많을 땐 낮음
      // 가지고 있는 큐브의 수가 많을 때 점수 높음, 적을땐 낮음
      // 가지고 있는 자원생산자의 수가 적을 때 점수 높음, 많을 때 낮음

      float score = 0;

      // 자원량이 100이하일 때 70점, 300이상일 때, 0점
      // score += Mathf.Clamp((1 - (ai.totalResource - 100) / 200) * 70, 0, 70);

      // 자원량이 70이하일 때 50점, 300이상일 때, 0점
      score += Mathf.Clamp((1 - (ai.totalResource - 70) / 230f) * 40, 0, 40);

      Debug.Log($"Resource Score : {score} , total : {ai.totalResource}");


      float myResource = 0;
      foreach(var resource in ai.haveBlock)
      {
          if(resource is ResourceProducer)
          {
              myResource++;
          }
      }


      // 자원생산자가 5개 이하면 30점, 12개 이상이면 0점
      score += Mathf.Clamp((1 - (myResource - 5) / 7f) * 30, 0, 30);


      float haveCube = 0;
      foreach(var cube in ai.haveBlock)
      {
          if(cube is Cube && cube is not Core)
          {
              haveCube++;
          }
      }
      // 자신의 큐브 수가 5개 이하면 0점 10개면 30점
      score += Mathf.Clamp((haveCube - 5) * 6, 0, 30);

      Debug.Log($"Resource Score : {score}");
          return Mathf.Clamp(score, 0, 100);
  }

처음 기준으로 자신의 자원량에 따라서 많을 때, 점수가 내려가고, 적으면 올라가는 점수부여 시스템을 만들었다.

두번째 기준으로 자신의 자원생산자 개수에 따라, 많으면 점수가 내려가고 적으면 올라간다.

마지막 기준으로, 자신에게 있는 큐브(코어 제외)들의 수를 확인하여 점수를 부여한다.

위치 점수 

기준을 3가지로, 

생성 방향(50점), 붙어있을 큐브 종류(20점), 공격받을 위험(30점) 이다.

    public override float EvaluatePosition(Vector3 dir, Transform cube, Vector3 pos)
    {
        // 기본적으로 뒤에 배치되면 좋음,
        // 자신 앞의 자기 큐브가 많을수록 좋음 
        // 자신과 같은 x축에 무기요소가 있으면 안좋음

        float score = 0;

        // 생성 방향에 따른 점수 부여
        if(dir == Vector3.forward)
        {
            score += 50;
        }
        else if(dir != Vector3.back)
        {
            score += 30;
        }

        Block block = cube.GetComponent<Block>();

        // 코어가 아닌 블럭의 경우 점수
        if(cube.GetComponent<Block>() is not Core)
        {
            score += 20;
        }

        // 붙을 블럭의 안정성 점수
        float nearCannon = 0;
        float totalDistance = 0;

        foreach (var cannon in cubeCreator.blockList)
        {
            // 플레이어 대포 카운팅
            if (cannon is Cannon && cannon.myCaster is InputController)
            {
                // 모든 대포와의 거리 합산
                float dis = Mathf.Abs(block.worldPosition.x - cannon.worldPosition.x) +
                            Mathf.Abs(block.worldPosition.y - cannon.worldPosition.y) +
                            Mathf.Abs(block.worldPosition.z - cannon.worldPosition.z);
                // 멀리있는 대포 제외
                if (dis <= 20)
                {
                    nearCannon++;
                    totalDistance += dis;
                }
            }
        }
        if(nearCannon > 0)
        {
            score += Mathf.Clamp(((totalDistance / nearCannon) / 20f) * 30, 0, 30);
        }
            // 안정성이 20이상일 경우 30점, 0이하일 경우 0점

            return Mathf.Clamp(score, 0, 100);
    }

먼저 생성 위치가, 적 코어 기준 플레이어의 코어 반대 방향인 forward로 향에 있다면 50점을 부여한다. 또한 플레이어 코어쪽 방향인 back이 아니라면 30점을 부여한다. 살짝 무식한 방법이긴 한데, 가장 효율적인 위치 점수이다.

두번째 기준으로 생성할 때, 붙어있을 큐브의 종류가 코어가 아닐 경우 20점을 부여한다. 이는 코어에 자원 생산자를 생성하여, 안잔성을 떨어트리는 일을 방지한다.

마지막 기준으로 defence에서 사용한 것처럼 생성위치와 인접한 플레이어의 대포들의 평균 거리를 측정하여, 점수로 반환한다. 평균 거리가 짧을 수록 점수가 낮으며 이러한 코드는 자원생산자가 안전한 위치에 생성되는 것을 원초적으로 한다.

EnemyAttack

행동 점수

대포 생성의 경우, 굉장히 점수를 짜게 줬다. 이는 AI가 정말 사람처럼 똑똑하게 플레이 할수 있게하며, 게임의 난이도를 올린다. 

대포 생성이 초반에 많이 일어날 경우, 코어의 안전성이 굉장히 떨어져서 게임이 굉장히 빨리 끝나거나, 자원량이 부족해서, 높은 레벨의 Enemy의 생성 속도를 따라오지 못하게 되는 경우가 존재하기 때문에, 이를 방지하는데 많이 할애했다.

Attack 대포 생성의 점수 기준은 자원량 (30점), 코어의 안전성 (50점), 플레이어 코어의 안전성(20점)으로 구현했다. 

    public override float EvaluateAction()
    {
        
        // 자원량이 많을수록 점수 높음, 낮을수록  낮음
        // 가지고 있는 큐브의 개수가 많을수록 점수 높음, 낮을수록 낮음
        // 상대의 공격블럭이 적을수록 점수 높음
        // 상대의 방어블럭이 적을수록 점수 높음

        float score = 0;

        //score += Mathf.Clamp((1 - (ai.totalResource - 50)/250) * 70 ,0, 70);


        // 300이상일 때 30점, 100점 이하일 때, 0점
        // score += Mathf.Clamp((ai.totalResource - 100 / 200) * 30, 0, 30);

        // 400이상일 때 30점, 200점 이하일 때, 0점
        score += Mathf.Clamp((ai.totalResource - 200) / 200f * 30, 0, 30);



        float nearCube = 0;
        foreach (var block in cubeCreator.blockList)
        {
            if (block is Cube && block.myCaster is EnemyAI && block is not Core) // 자신의 큐브들 (코어 제외)
            {
                if (ai.core.worldPosition.x - Mathf.Abs(block.worldPosition.x) <= 2) // 코어의 2칸 이내에 존재한다
                {
                    nearCube++;
                }
                else if (ai.core.worldPosition.y - Mathf.Abs(block.worldPosition.y) <= 2) // 코어의 2칸 이내에 존재한다
                {
                    nearCube++;
                }
                else if (ai.core.worldPosition.z - Mathf.Abs(block.worldPosition.z) <= 2) // 코어의 2칸 이내에 존재한다
                {
                    nearCube++;
                }

            }
        }
        // 코어 주변 큐브가 6개일 때 10점, 10개일 때 50점
        // score += Mathf.Clamp((nearCube - 5) * 10, 0, 50);

        // 코어 주변 큐브가 6개일 때 10점, 15개일 때 50점
        score += Mathf.Clamp((nearCube - 5) * 5, 0, 50);


        // float playerCannon = 0; // 상대 진영의 대포 개수
        // float playerCube = 0; // 상대 진영의 큐브 개수

        Core playerCore = null;

        // 플레이어 코어 찾기
        foreach(var block in cubeCreator.blockList)
        {
            if(block is Core && block.myCaster is InputController) // 상대 큐브의 수
            {
                playerCore = block.GetComponent<Core>();
            }
        }

        foreach (var block in cubeCreator.blockList)
        {
            if (block is Cube && block.myCaster is InputController && block is not Core) // 자신의 큐브들 (코어 제외)
            {
                if (playerCore.worldPosition.x - Mathf.Abs(block.worldPosition.x) <= 3) // 코어의 3 이내에 존재한다
                {
                    nearCube++;
                }
                else if (playerCore.worldPosition.y - Mathf.Abs(block.worldPosition.y) <= 3) // 코어의 3칸 이내에 존재한다
                {
                    nearCube++;
                }
                else if (playerCore.worldPosition.z - Mathf.Abs(block.worldPosition.z) <= 3) // 코어의 3칸 이내에 존재한다
                {
                    nearCube++;
                }

            }
        }
        // 플레이어 큐브의 수가 6개일 때 4점 10개일 때, 20점
        score += Mathf.Clamp((nearCube - 5) * 4, 0, 20);

        Debug.Log($"Attack Score : {score}");
        return Mathf.Clamp(score, 0, 100);
    }

가장 먼저 자신의 자원량에 따라서, 점수를 부여한다. 자원이 확실히 충분할 때에만 대포를 생성할 수 있게 값을 많이 조절하였다.

두번째 기준으로 자신의 코어 안전성을 확인하여,  정말 안전하다고 판단될때에 점수를 부여한다. EnemyDefence에서는 코어 기준 x,y,z 합차가 3칸이였는데, 여기서는 2칸으로 하여 정말 최소한의 안전성이 있는지 확인한다.

마지막 기준으로, 플레이어 코어의 안전성을 확인하느데, 플레이어 코어 기준 주변에 있는 큐브의 수에 따라서 점수를 부여한다.

위치 점수

대포의 위치 점수 기준은 크게 3가지로,

생성 방향(50점), 붙어있는 큐브의 종류(20점), 공격이 가능한 상대 큐브의 개수(30점)으로 나뉜다.

public override float EvaluatePosition(Vector3 dir, Transform cube, Vector3 pos)
{
    // 기본적으로 무조건 앞에 배치되야함 (방향)
    // 앞에 자신의 블럭이 없어야 좋음,
    // 앞에 코어가 있으면 매우 좋음
    // 앞에 상대 블럭이 많으면 좋음

    float score = 0;

    if(dir == Vector3.back) // 적 기준 뒤 방향
    {
        score += 50;
    }

    

    Block block = cube.GetComponent<Block>();

    if (block is not Core)
    {
        score += 20;
    }

    float xRange = 0;
    float yRange = 0;
    float zRange = 0;

   if(dir == Vector3.back)
    {
        xRange = 6;
        yRange = 6;
        zRange = -10;
    }
   else if (dir == Vector3.forward)
    {
        xRange = 6;
        yRange = 6;
        zRange = 10;
    }
    else if (dir == Vector3.right)
    {
        xRange = 10;
        yRange = 6;
        zRange = 6;
    }
    else if (dir == Vector3.left)
    {
        xRange = -10;
        yRange = 6;
        zRange = 6;
    }
    else if (dir == Vector3.up)
    {
        xRange = 6;
        yRange = 10;
        zRange = 6;
    }
    else if (dir == Vector3.down)
    {
        xRange = 6;
        yRange = -10;
        zRange = 6;
    }


    float cubeNum = 0;
    foreach(var myBlock in cubeCreator.blockList)
    {
        // 상대 큐브 검사
        if(myBlock is Cube && myBlock.myCaster is InputController)
        {
            if (dir.x != 0)
            {
                if(dir.x == -1)
                {
                    if (myBlock.worldPosition.x >= block.worldPosition.x + xRange && myBlock.worldPosition.x <= block.worldPosition.x &&
                        myBlock.worldPosition.y >= block.worldPosition.y - yRange && myBlock.worldPosition.y <= block.worldPosition.y + yRange &&
                        myBlock.worldPosition.z >= block.worldPosition.z - zRange && myBlock.worldPosition.z <= block.worldPosition.z + zRange)
                    {
                        cubeNum++;
                    }
                }
                else if(dir.x == 1)
                {
                    if (myBlock.worldPosition.x <= block.worldPosition.x + xRange && myBlock.worldPosition.x >= block.worldPosition.x &&
                        myBlock.worldPosition.y >= block.worldPosition.y - yRange && myBlock.worldPosition.y <= block.worldPosition.y + yRange &&
                        myBlock.worldPosition.z >= block.worldPosition.z - zRange && myBlock.worldPosition.z <= block.worldPosition.z + zRange)
                    {
                        cubeNum++;
                    }
                }
            }
            else if (dir.y != 0)
            {
                if (dir.y == -1)
                {
                    if (myBlock.worldPosition.x >= block.worldPosition.x - xRange && myBlock.worldPosition.x <= block.worldPosition.x + xRange&&
                        myBlock.worldPosition.y >= block.worldPosition.y + yRange && myBlock.worldPosition.y <= block.worldPosition.y &&
                        myBlock.worldPosition.z >= block.worldPosition.z - zRange && myBlock.worldPosition.z <= block.worldPosition.z + zRange)
                    {
                        cubeNum++;
                    }
                }
                else if (dir.y == 1)
                {
                    if (myBlock.worldPosition.x >= block.worldPosition.x - xRange && myBlock.worldPosition.x <= block.worldPosition.x + xRange &&
                        myBlock.worldPosition.y <= block.worldPosition.y + yRange && myBlock.worldPosition.y >= block.worldPosition.y &&
                        myBlock.worldPosition.z >= block.worldPosition.z - zRange && myBlock.worldPosition.z <= block.worldPosition.z + zRange)
                    {
                        cubeNum++;
                    }
                }
            }
            else if (dir.z != 0)
            {
                if (dir.z == -1)
                {
                    if (myBlock.worldPosition.x >= block.worldPosition.x - xRange && myBlock.worldPosition.x <= block.worldPosition.x + xRange &&
                        myBlock.worldPosition.y >= block.worldPosition.y - yRange && myBlock.worldPosition.y <= block.worldPosition.y + yRange&&
                        myBlock.worldPosition.z >= block.worldPosition.z + zRange && myBlock.worldPosition.z <= block.worldPosition.z)
                    {
                        cubeNum++;
                    }
                }
                else if (dir.z == 1)
                {
                    if (myBlock.worldPosition.x >= block.worldPosition.x - xRange && myBlock.worldPosition.x <= block.worldPosition.x + xRange &&
                        myBlock.worldPosition.y >= block.worldPosition.y - yRange && myBlock.worldPosition.y <= block.worldPosition.y + yRange &&
                        myBlock.worldPosition.z <= block.worldPosition.z + zRange && myBlock.worldPosition.z >= block.worldPosition.z)
                    {
                        cubeNum++;
                    }
                }
            }
        }
    }
    Debug.Log($"pos : {pos} , dir : {dir} , cubeNum : {cubeNum}");

    // 공격가능한 큐브가 10개일 때, 30점 0개일 때 0점
    score += Mathf.Clamp(cubeNum * 3, 0, 30);

        return Mathf.Clamp(score, 0, 100);
}

먼저 방향에 따른 점수를 부여하는데, 대체로 플레이어 코어가 있는 방향인 Vector3.back이 생성 방향일 때, 50점을 부여한다.

두번째 기준으로 붙어있을 큐브가 코어가 아닐 때, 20점을 부여한다. 이는 코어의 안전성을 더 올리기 위한 기준이다.

마지막 기준으로, 생성할 때, 자신이 공격할 수 있는 큐브의 개수를 판단하여 점수를 부여한다. 먼저 생성 방향을 구하고, 이에 따라 공격범위를 구해낸다. 이후, 큐브가 해당 범위내에 있을 경우 cubeNum에 추가하고, 이 cubeNum의 값에 따라 점수를 추가하는 식이다. 매우 복잡하고 일차원적으로 만들었는데, 이 코드를 작성할 때, Vector를 조금 공부해야겠다고 생각했다..

EnemyAI

canPosNum은 생성 가능한 위치를 뜻하는데, 이 부분을 수정해주었다.

        if(canPosNum <= 5)
        {
            action = actionList.Find(a => a is EnemyDefence); // action에 EnemyDefence 강제로 채우기
        }

기존 2에서 5로 늘려 확장 위주로 AI가 행동하도록 한다.

TutorialManager

테스트를 할 때, 튜토리얼이 번거로워서 잠시동안 코드를 변형시켜 사용했다. 이때, 나오는 버그를 조금 수정하였다.

   	    // public bool isTutorial { get; private set; } = true; // 튜토리얼 상태인지 판단
    public bool isTutorial = false; // 테스트용
    private void Update()
    {
        if(isTutorial)
        {
            if (myState == TutorialState.CAMERAMOVE)
            {
                cameraMoveBar.fillAmount = totalCameraMove / maxCameraMove;
                cameraScrollBar.fillAmount = totalCameraScroll / maxCameraScroll;

                if (totalCameraMove >= maxCameraMove && totalCameraScroll >= maxCameraScroll)
                {
                    ResourceTutorial();
                }
            }

        }
    }

Update에서 isTutorial 조건문을 추가해주었다. 이게 없었을 때, ResourceTutorial로 넘어가는 경우가 생겨서 불편한 점이 있었다.

 

Cannon

가장 가까운 대상을 공격하지 않는 버그가 있었다.

            float bestDistance = 100;

            foreach (Collider collider in hits)
            {
                Cube cube = collider.GetComponent<Cube>();

                if (cube != null && myCaster != cube.myCaster) // 대상이 큐브이고, 자신과 시전자가 다르다면
                {
                    float distance = (cube.transform.position - transform.position).magnitude;
                    Debug.Log($"distance : {distance}");
                    if (bestDistance > distance)
                    {
                        bestDistance = distance;
                        target = cube;
                    }
                    Debug.Log(collider.name + " 거리 : " + distance);
                }

bestDistance를 반복문 밖에 선언하여 수정해주었다.

완성본 (Random을 사용하지 않을 때)

확실히 Enemy가 Cube 위주로 생성하며, 자원 생산자와 대포의 위치또한 적절한 것을 볼수 있다.

가장 중요한 코어의 안전성이 매우 높아진 것을 볼수 있다. AI가 똑똑해졌다~

'GameDev > Cubidom' 카테고리의 다른 글

Cubidom - AI의 제약 시스템  (0) 2026.05.09
Cubidom - 제약 시스템  (0) 2026.05.07
Cubidom - 디테일 추가 및 버그 수정 2  (0) 2026.04.30
Cubidom - 디테일 추가 및 버그 수정 1  (0) 2026.04.29
Cubidom - FTUE 3  (0) 2026.04.28