ゲーム制作勉強中!あこがれだったプログラマーに今からなろう!

昔、あこがれていたプログラマー。今からでも勉強してみようと思い立ち、チャレンジ開始! 勉強メモや、悪戦苦闘な日々の記録です。

ウィザードリィライクのダンジョンゲームを作る!(4):Playerの移動に合わせてダンジョンの描画範囲を移動させる。



前回で、ダンジョンマップの設計図classのDungeonMap-classの改造と、DungeonManager-classへのObjectPoolの追加が完了したので、いよいよPlayerの移動に合わせて、ダンジョンの描画範囲を移動させる様にしていこうと思います。

描画範囲を移動させるには、Playerの向いている方向や移動方向を把握する必要があるため、PlayerController-classに管理用の変数を追加します。

    int player_Direction;//プレイヤーの向き 0:北 1:東 2:南 3:西
     Vector3Int targetPosition;

方向管理は最初enumを組んでいたのですが、簡単とはいえ計算が必要になるので、ちょっと悩んで、結局、数値に戻してしまいました🤪
enumを組んだほうが可読性とやらは上がると思うのですが、個人制作で、しかも方向管理程度なら、コメントで補足さえすれば、数値のままでも良いかなと思います。😅

他には、些細な変更ですが、将来、ユーザーが設定を変更できるようにコンフィグ画面を作成する時に備えて、暫定でGameManagerに作成しておいた、Playerの移動速度などの設定値関係を、今の内に単独のManager、ParameterManager-classを作成して移します。

さて、肝心の移動に沿って描画範囲を移動させる仕組みですが、要は、
Playerの移動先の位置を基準に、移動方向の描画範囲の末端 1グリッド分を左右に描画範囲分新たに書き足して、
Plyaerの移動前の位置を基準に、移動方向とは逆の描画範囲の末端1グリッド分を左右に描画範囲分消してやれば良いので



まずはPlayerが東西南北のどちらに向かって移動したのかが、どうしても必要になります。
Playerは後向きに進む事もできる様にするので、Playerの向きを管理しつつ、今、移動しようとしている方向を計算する必要があるわけです。
前に移動する時は、Playerの向きがそのままPlayerの移動方向になるので簡単ですが、
後ろに移動する時はPlayerの向きの逆なので、この様な方法で算出します。

 int Walking_Direction = (player_Direction -2);

 if(Walking_Direction < 0)
 {
  Walking_Direction = 4 + Walking_Direction; 
 }


移動方向が特定できたら、ようやく移動の処理の流れです。

① 入力操作から移動方向を計算する。

② 移動方向と現在位置、前方Vector(transform.forward)を、DungeonManager-classに渡した後、Playerの移動処理を行います。

③ 受け取った値から、描画する行(DrawLine)と消去する行(EraseLine)を計算し、描画と消去を行うグリッドを決めます。

④ 描画と消去を行うグリッドから、処理を行う座標を計算します。

⑤ 指定された座標のObjectの有無をチェックして描画/消去を行います。

これだけだと、よく分からないので、順を追って説明します。

まず、「① 入力操作から移動方向を計算する。」
入力操作があるとWalk関数は、Playerの移動方向と移動後の座標(targetPosition)を計算し、Walking関数に渡します。

    void Update()
    {
        //InputSystemから左スティックに指定されたデバイスの入力を受け取る。
        Vector2 LeftDirction=ISystem.Player.Move.ReadValue<Vector2>();

        if(!IsRotate && !IsWalk)//回転中・移動中は今のアクションを継続する。
        {
            Rotation(LeftDirction.x);
            Walk(LeftDirction.y);
        }
    }

    void Walk(float y)
    {
        if(y > 0.4f)
        {
            //transform.forward方向がtargetPosition(移動後の位置)になる。
            targetPosition = 
                        new Vector3Int((int)transform.position.x,0,(int)transform.position.z) + 
                        new Vector3Int((int)Math.Round(transform.forward.x * 2), 0,
                        ((int)Math.Round(transform.forward.z * 2)));

            Walking(player_Direction);//Plyaerの向きをWalking関数に送る。
        }
        else if(y < -0.4f)
        {
            //Playerの向きの逆向きを計算する。
            int Walking_Direction = (player_Direction -2);

            if(Walking_Direction < 0)
            {
                Walking_Direction = 4 + Walking_Direction;
            }

            //transform.forwardの逆側の方向がtargetPosition(移動後の位置)になる。
            targetPosition = 
                        new Vector3Int((int)transform.position.x,0,(int)transform.position.z) - 
                        new Vector3Int((int)Math.Round(transform.forward.x * 2), 0,
                        ((int)Math.Round(transform.forward.z * 2)));

            Walking(Walking_Direction);//Plyaerの向きの逆向きをWalking関数に送る。
        }
    }

①の段階でPlayerの前後移動の違いを整理して、以降の処理は単純に移動した方向で考えることができます。


「② 移動方向と現在位置、前方Vector(transform.forward)を、DungeonManager-classに渡した後、Playerの移動処理を行います。」

Walk関数から移動方向を受け取ったWalking関数は、Playerの現在位置(transeform.position)とtransform.forwardと一緒に、DungeonManager-classBuild_By_Move関数に渡した後、Playerの移動処理を行います。

    async void Walking(int player_direction)
    {
        IsWalk = true;

        dmanager.Build_By_Move(
                new Vector3Int((int)Math.Round(transform.position.x),0,(int)Math.Round(transform.position.z)),
                new Vector3Int((int)Math.Round(transform.forward.x),0,(int)Math.Round(transform.forward.z)),
                player_direction);

        //ここから下はPlayerの移動処理
        while(Vector3.Distance(transform.position, targetPosition) > Mathf.Epsilon)
        {
            transform.position = Vector3.MoveTowards(transform.position, targetPosition, pmanager.PlayerSpeed * Time.deltaTime);
            await UniTask.Yield();
        }

        transform.position = new Vector3((int)targetPosition.x, 0, (int)targetPosition.z);

        IsWalk = false;
    }

実は、この段階では①と②で処理を分ける必要はあまりないのですが、後々、Walk関数Walking関数の間で、壁チェックを入れることになるので、今の段階で分けています。


「③ 受け取った値から、描画する行(DrawLine)と消去する行(EraseLine)を計算し、描画と消去を行うグリッドを決めます。」

ここからDungeonManager-classでの、実際のダンジョンの描画と消去の処理になります。
Build_By_Move関数は、受け取った値から、描画する行(DrawLine)と消去する行(EraseLine)を計算し、DrawLineをグリッド毎にDungeonGenerator関数に描画を、EraseLineをグリッド毎にDungeonEraser関数に消去を、それぞれ依頼します。

DungeonMapでは、Playerの存在できるx,z共に奇数座標と、その北と東の壁を含んで1グリッドとして扱っているので、描画範囲を計算する場合はdrawingSizeの2倍で良いことになります。

要は、北に行く時は(x,0,z)のzの+方向に、drawingSize * 2の位置が描画範囲の末端1グリッドになる。と言う理屈です🤣

そして、描画する行(DrawLine)は、Playerの「移動先の位置を基準」に、描画範囲(drawingSize)の末端 1グリッド なので

DrawLine = new Vector3(targetPosition.x,0,targetPosition.z + (pmanager.drawingSize * 2)); 

という感じになり、
「消去する行(EraseLine)」は、Plyaerの「移動前の位置を基準」に、描画範囲の末端1グリッド  なので
先ほどの北に行く例では、逆にzを-方向にdrawingSize * 2になるので、

EraseLine = new Vector3(Playerのtransform.position.x,0,Playerのtransform.position.z - (pmanager.drawingSize * 2));

になります。
これで、描画する行(DrawLine)と消去する行(EraseLine)が計算できたので、これを「描画範囲の末端 1マス分を左右に描画範囲分」するためにfor文で回して、次の関数に送ってやります。

for(int i = - (pmanager.drawingSize ) * 2 ;
       i < (pmanager.drawingSize + 1) * 2 ;
       i +=2)
       {
           Dungeon_Generator(((int)DrawLine.x + i), (int)DrawLine.z);
           Dungeon_Eraser(((int)EraseLine.x + i), (int)EraseLine.z);
       }

これで基本はできたのですが、Playerが移動する方向によって、x,0,zのどこに増減するか変わるので、PlayerContoroller-classから受け取ったPlayerの移動方向を使って、Switchで分岐して処理を変えていきます。
なので、Build_By_Move関数の全体は

    public void Build_By_Move(Vector3Int P_position,Vector3Int P_foward, int direction)
    {
      Vector3 DrawLine;
      Vector3 EraseLine;

      Vector3Int targetPosition = P_position + new Vector3Int((int)(P_foward.x * 2), 0,
                        (int)(P_foward.z * 2)); 

      switch (direction)
      {
        case 0://北
          DrawLine = new Vector3(targetPosition.x,0,targetPosition.z + (pmanager.drawingSize * 2)); 
          EraseLine = new Vector3(P_position.x,0,P_position.z - (pmanager.drawingSize * 2));

          for(int i = - (pmanager.drawingSize ) * 2 ;
                i < (pmanager.drawingSize + 1) * 2 ;
                    i +=2)
          {
            Dungeon_Generator(((int)DrawLine.x + i), (int)DrawLine.z);
            Dungeon_Eraser(((int)EraseLine.x + i), (int)EraseLine.z);
          }
          break;

        case 1://東
          DrawLine = new Vector3(targetPosition.x + (pmanager.drawingSize * 2),0,targetPosition.z);
          EraseLine = new Vector3(P_position.x - (pmanager.drawingSize * 2),0,P_position.z); 

          for(int i = - (pmanager.drawingSize ) * 2 ;
                i < (pmanager.drawingSize + 1) * 2 ;
                    i +=2)
          {
            Dungeon_Generator((int)DrawLine.x, (int)DrawLine.z + i);
            Dungeon_Eraser((int)EraseLine.x, (int)EraseLine.z + i);
          }
          break;

        case 2://南
          DrawLine = new Vector3(targetPosition.x,0,targetPosition.z - (pmanager.drawingSize * 2));
          EraseLine = new Vector3(P_position.x,0,P_position.z + (pmanager.drawingSize * 2));

          for(int i = - (pmanager.drawingSize ) * 2 ;
                i < (pmanager.drawingSize + 1) * 2 ;
                    i +=2)
          {
            Dungeon_Generator((int)DrawLine.x + i, (int)DrawLine.z);
            Dungeon_Eraser((int)EraseLine.x + i, (int)EraseLine.z);
          }
          break;


        case 3://西
          DrawLine = new Vector3(targetPosition.x - (pmanager.drawingSize * 2),0,targetPosition.z); 
          EraseLine = new Vector3(P_position.x + (pmanager.drawingSize * 2),0,P_position.z);

          for(int i = - (pmanager.drawingSize ) * 2 ;
                i < (pmanager.drawingSize + 1) * 2 ;
                    i +=2)
          {
            Dungeon_Generator((int)DrawLine.x, (int)DrawLine.z + i);
            Dungeon_Eraser((int)EraseLine.x, (int)EraseLine.z + i);
          }
          break;
      }
    }

になります。


「④ 描画と消去を行うグリッドから、処理を行う座標を計算します。
ここは処理するマスを、座標レベルに分解してやる工程で、
描画は今までに使用していたDungeon_Generator関数を、消去はこれによく似たDungeon_Eraser関数で行います。

    private void Dungeon_Generator(int x, int y)
    {
        Check_And_Build(x, y, 0);//床と天井
        Check_And_Build(x, y + 1, 90);//北壁
        Check_And_Build(x + 1, y, 0);//東壁
    }

    private void Dungeon_Eraser(int x, int y)//消す工程なので角度は不要。
    {
        Check_And_Erase(x, y);//床と天井
        Check_And_Erase(x, y + 1);//北壁
        Check_And_Erase(x + 1, y);//東壁
    }



最後の「⑤ 指定された座標のObjectの有無をチェックして描画/消去を行います。」

座標と処理に分解された最終段階になります。
描画する場合でも消去する場合でも、まずは、指定された座標が壁の座標なのかそれともPlayerが存在する座標なのかをチェックします。
Playerが存在する座標はx軸z軸ともに奇数のため、x軸、z軸のどちらかが偶数であれば、壁の座標と判断できます。

描画を行う場合は、指定座標が壁の座標であれば、その座標のObjectの数を確認して、0なら描画を行い、1以上のObjectが存在する場合は描画を行わないようにしています。
Playerが存在する座標の場合は、その座標のObjectの数を確認して、0もしくは1なら描画を行い、2以上のObjectが存在する場合は描画を行いません。
これは、壁の座標には基本的にはObjectは1つしか存在しません(子Objectはこの時点では無視。)が、Playerが存在する座標は床と天井の2つのObjectが存在するためです。
消去を行う場合は、その座標のDungeonMapのmapからその座標のObjectの種類を、objectsからはその座標に存在するObjectそのものを、それぞれ取得してそのObjectの種類に応じたObjectPoolに返却しています。
消去を行う場合も返却するObjectの数が変わるので、壁の座標なのかPlayerの座標なのかのチェックは必要です。

    private void Check_And_Build(int posX, int posY, int rotation)
    {
      int getMapchip = map.Get(posX, posY);

        if(posX % 2 == 1 && posY % 2 == 1 &&
          (map.GetObject(posX, posY) == null || map.GetObject(posX, posY).Count == 1))
          //奇数座標なら天井と床を生成。
        {
          switch (getMapchip)
          {
            case 0:
              GetFromPool(0,posX,0,posY,rotation);//床
              GetFromPool(0,posX,2,posY,rotation);//天井

              break;

            case 1:
              break;

            default:
              break;
          }
        }
        else//偶数座標なら壁を生成。
        {
          if(getMapchip != -1 && getMapchip != 0 && map.GetObject(posX, posY) == null)
          {
              GetFromPool(getMapchip,posX,0,posY,rotation);//99は壁、98は扉
          }
        }
    }


    private void Check_And_Erase(int posX, int posY)
    {
      int getMapchip = map.Get(posX, posY);

      if(map.GetObject(posX, posY) != null)
      {
        if(posX % 2 == 1 && posY % 2 == 1)
        {
          switch (getMapchip)
          {
            case 0:
              foreach (var obj in map.GetObject(posX, posY))
              {
                ReturnToPool(map.Get(posX, posY),obj);
              }

              map.ListClear(posX, posY);
              break;

            case 1:
              break;

            default:
              break;
          }
        }
        else//偶数座標なら壁を返却。
        {
          foreach (var obj in map.GetObject(posX, posY))
          {
            ReturnToPool(map.Get(posX, posY),obj);
          }

          map.ListClear(posX, posY);
        }
      }
    }


これでPlayer移動に合わせてダンジョンの描画範囲も移動するようになりました。
ですが、まだ壁を貫通して移動できてしまっていますので、次回は壁を越えられないようにするのと、扉を開けれるようにしたいと思います。









  翻译: