《占地为王》代码清单

游戏主要参考泡泡堂,就占地盘,为大二下学期期末作业。

需求:多人游戏,多人Ui,四个地图,四个道具,有练习模式等等。

源码已上传 (github

UnityVersion : 2020.3.19f1c2.

主要重点

1.根本玩法填涂色块之色块衔接

  • 需求分析:色块衔接的流动性,加分以及减去其他玩家分数,角色自己色块衔接的同时影响其他角色的色块

  • 为了更好的实现目标设计了以下程序和结构

    • 声明一个父类BasicPlayer静态字典public static Dictionary<PlayerFlag, TileBase[][]> playTile;键为角色的枚举PlayerFlag,值为每一个子类所特有的用来保存瓦片的交错数组public TileBase[][] Tiles;

    • 色块一共有七种,考虑方向大概有每名角色有20不同色块

      • 直接上图图1

      图2

      大致代码

      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
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      public void JudegNowPos(TileBase[][] tiles,Vector3 pos,ref PlayerFlag oldFlag)
      {
      TileBase tile = tilemap.GetTile(tilemap.WorldToCell(pos));
      if (!ContainsTile(tiles, tile))
      {
      scores++;
      foreach (PlayerFlag p in playTile.Keys)
      {
      if (p == this.playerFlag) continue;
      if (ContainsTile(playTile[p],tile))
      {
      PlayerManager.Instance.playingPlayers[(int)p-1].GetComponent<BasicPlayer>().scores--;
      oldFlag = p;
      }
      }
      }
      }
      //对玩家上下左右进行色块判定
      public void TestFloor(TileBase[][] tiles,Vector3 pos)
      {
      int[] dir = { 0, 0, 0, 0 };
      int num = 0;
      for(int i=0;i<4; i++)
      {
      if (ContainsTile(tiles, tilemap.GetTile(tilemap.WorldToCell(pos+vector3[i]))))
      {
      dir[i]++;
      num++;
      }
      }
      switch (num)
      {
      case 0: tilemap.SetTile(tilemap.WorldToCell(pos), tiles[num][0]); break;
      case 1:
      int i;
      for (i = 0; i < 4; i++)
      {
      if (dir[i] != 0) break;
      }
      tilemap.SetTile(tilemap.WorldToCell(pos), tiles[num][i]);
      break;
      case 2:
      if (dir[0] == dir[2]&&dir[0]==1) tilemap.SetTile(tilemap.WorldToCell(pos), tiles[num][0]);
      else if (dir[1] == dir[3]&&dir[1]==1) tilemap.SetTile(tilemap.WorldToCell(pos), tiles[num][1]);
      else
      {
      int k ;
      bool exist = false;
      for ( k = 0; k < 4; k++)
      {
      if (dir[k%4] == dir[(k + 1)%4]&&dir[k]==1)
      {
      exist = ContainsTile(tiles, tilemap.GetTile(tilemap.WorldToCell(pos + vector3[k+4])));
      break;
      }
      }
      if(!exist) tilemap.SetTile(tilemap.WorldToCell(pos), tiles[5][k]);
      else tilemap.SetTile(tilemap.WorldToCell(pos), tiles[6][k]);
      }
      break;
      case 3:
      int j ;
      for (j = 0; j < 4; j++)
      {
      if (dir[j] == 0) break;
      }
      tilemap.SetTile(tilemap.WorldToCell(pos), tiles[num][j]);
      break;
      case 4: tilemap.SetTile(tilemap.WorldToCell(pos), tiles[num][0]); break;
      }
      }
      public void TestAroundFloor(TileBase[][] tiles,Vector3 pos ,PlayerFlag oldFlag)
      {
      for(int i=0;i<vector3.Length;i++)
      {
      Vector3 vector = pos + vector3[i];
      TileBase tile = tilemap.GetTile(tilemap.WorldToCell(vector));
      if (ContainsTile(tiles, tile))
      {
      TestFloor(tiles, vector);
      }
      else if(oldFlag != PlayerFlag.NoHost&& ContainsTile(playTile[oldFlag],tile))
      {
      TestFloor(playTile[oldFlag],vector);
      }
      }
      }```

2.四个地图

  • 猪图 生成猪群冲撞,协程控制生成,传入isRight参数控制生成方位和重写动画状态机
1
2
3
4
5
6
7
8
9
10
11
12
13
IEnumerator CreatePig(bool IsRight)
{
yield return new WaitForSeconds(5f);
while(true)
{
for(int i=0;i<num; i++)
{
Instantiate(pig).GetComponent<PigRush>().isRight = IsRight;
IsRight = !IsRight;
yield return new WaitForSeconds(0.5f);
}
yield return new WaitForSeconds(rushTime);
}
  • 冰图 冰面滑步效果,玩家进入冰面Trigger切换其移动方式为刚体施力。
1
2
3
4
5
6
7
8
9
if(other.CompareTag("Icy"))
{
moveMode = 1;
}
public void MoveByMoveMode(int m)
{
if(m==0) rigidBody.MovePosition(rigidBody.position + inputMove * speed * Time.fixedDeltaTime);
else rigidBody.AddForce(inputMove * speed );
}
  • 糖图 就是简单的随机数生成,如果该position没有障碍物,则生成。

3.四个道具

  • 飞刀 实现很简单,如果碰到了敌人,切换敌人脚本上的色块就好

    方向根据释放道具玩家脚本上保留的“正前方”Forward来改变

      public void Movement(InputAction.CallbackContext ctx)
     {       
          if(ctx.performed)
          {
              inputMove = ctx.ReadValue<Vector2>();
              animPlayer.SetFloat("Vertical", inputMove.y);
              animPlayer.SetFloat("Horizonal", inputMove.x);
              Forward = inputMove;
               }
    if(ctx.canceled)
      {
          float angle = Vector2.SignedAngle(Vector2.right, inputMove);
          if (angle > -45 && angle <= 45) Forward = Vector2.right;
          else if (angle > 45 && angle <= 135) Forward = Vector2.up;
          else if (angle > 135 || angle <= -135) Forward = Vector2.left ;
          else if(angle > -135 && angle <= -45) Forward = Vector2.down;
          inputMove = new Vector2(0, 0);
      }
    }
    
  • 跑鞋 移速加倍,可穿过障碍物,对手,糖,和猪,

    • 因为不能使玩家穿透空气墙故通过改变layer的方式来改变碰撞,此前需要在ProjectSetting里设置layer之间的碰撞关系。

      1
      2
      3
      4
      5
      6
      7
      speed = 10;
      GameObject BoostEffect = Resources.Load<GameObject>("Effect/BoostEffect");
      Destroy(Instantiate(BoostEffect, transform.position, Quaternion.identity,transform), boostTime);
      gameObject.layer = LayerMask.NameToLayer("Boost");
      yield return new WaitForSeconds(boostTime);
      speed = 5;
      gameObject.layer = LayerMask.NameToLayer("Default");
  • 透明药水 实现简单,改变其他玩家状态,无法涂色

  • 炸弹 实现简单,随机数将地图上的瓦片替换为施放者去角色瓦片即可

4.多人游戏和多人UI控制

  • 多人UI控制

    开始游戏只生成一名MultiplayeEventSystem并保存下来,选择角色时其余玩家通过按键来生成自己定义的UIcontroler预制体(绑定了UI控制组件),之后使其失去选择物直到准备界面再重新赋予其选择物,在unity的组件帮助下还算便利。

  • UI层级变化 定义了一个Ui栈 ,到下一层则令Stack.Peek()失活,并使下一层进栈,返回上一层则令其先出栈且使其失活,在激活栈顶物体

  • 多人游戏在PlayerinputManager的帮助下比较简单,值得注意的就是要保留生成UIcontroler时的设备.

    这是三种获取输入设备的方法
    //Debug.Log(playerInput.user.pairedDevices[0]);
    // Debug.Log(playerInput.devices[0] );
    //Debug.Log(playerInput.GetDevice<Gamepad>());
     然后再开始游戏时通过这样来生成角色
     public static PlayerInput Instantiate(GameObject prefab, int playerIndex = -1, string controlScheme = null, int splitScreenIndex = -1, params InputDevice[] pairWithDevices);
    

其余功能与细节

  1. 玩家选择人物问题

    • 问题1:如果使用按钮的点击回调事件,导致每名玩家都需要给四个角色按钮动态绑定点击函数,以至于每次点击一次该按钮下绑定的四个函数都会调用,更何况事件内的函数调用与否无法控制,结果就是四个人选的一样的人物,所以通过人为检测的办法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      private void BindCharacter(InputAction.CallbackContext ctx)
      {
      if(ctx.action.name=="Submit")
      {
      if (eventSystem.currentSelectedGameObject == null) return;
      else if (Opening.StackPeek.name== "ChooseChar" && ctx.started)
      {
      switch (eventSystem.currentSelectedGameObject.name[6])
      {
      case '1': PlayerManager.Instance.joinPlayers[DeviceId] = PlayerManager.Instance.players[0]; break;
      ......
      }
      eventSystem.currentSelectedGameObject.GetComponent<Button>().interactable = false;
      eventSystem.sendNavigationEvents = false;
      PlayerManager.Instance.existChar[DeviceId] = true;
      }
      }
      }
    • 还是选择人物问题,如果此时两名玩家选择的物体是相同的,一名玩家点击确认,使得该角色按钮Interactable取消,导致另一名玩家EventSystem大几率选择物丢失,或者选择为空, UI Navigation报错,解决办法如下,递归实现

      1
      2
      3
      4
      5
      6
      7
      8
      private void SetSelectinChooseChar(Button button,int length)
      {
      int i = button.gameObject.name[6]-'0';
      if (!button.interactable)
      SetSelectinChooseChar(Opening.Buttons[i%length],length);
      else
      eventSystem.SetSelectedGameObject(button.gameObject);
      }
  2. 观察者模式和单例模式的应用

    单例脚本PlayerManager(切换场景不可销毁),用来管理玩家和UI,关卡等关系,同时也是一名观察者,定义了event BeginPlay(),选择地图时添加委托,在每局准备界面玩家皆准备时自动调用,也要注意每局结束要删除之前添加的事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    例如         BeginPlaying -= LoadPigEvent;
    BeginPlaying -= LoadIceEvent;
    BeginPlaying -= LoadCandyEvent;
    BeginPlaying -= LoadBlankEvent;
    //显示Ready画面
    transform.GetChild(0).GetChild(0).gameObject.SetActive(true);
    transform.GetChild(0).GetChild(1).gameObject.SetActive(true);
    SetSelectInReadyMenu();
    BasicPlayer.playTile.Clear();
    int x = Random.Range(0, 4);
    switch (x)
    {
    case 0:
    BeginPlaying += LoadPigEvent;
    MapIntroduction.sprite = Resources.Load<Sprite>("MapIntroduction/Pigmap");
    SceneManager.LoadScene("PigMapTest", LoadSceneMode.Additive);
    break;
    ......

    }

    (单例虽好,但不要滥用,有时候记得清理单例对象的变量,同时时时刻刻清楚其生命周期,如同对待静态变量对待他)

  3. 数据库

    用于保存玩家获胜,获胜局数,最高分,总分等信息

  4. 场景异步加载以及它的加载完成后的委托事件应用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    例如:
    public void LoadBlankEvent()
    {
    AsyncOperation aa = SceneManager.LoadSceneAsync("Blank");
    aa.completed += EventAfterSceneLoaded;
    }
    private void EventAfterSceneLoaded(AsyncOperation a)
    {
    ......
    }
  5. 练习模式

    首先添加附加场景

    1
    SceneManager.LoadScene("BlankTest", LoadSceneMode.Additive);

    练习场景里的Camera设Targic设置为Renderer Texture与Ui上的rawimage对应就好