《占地为王》代码清单
游戏主要参考泡泡堂,就占地盘,为大二下学期期末作业。
需求:多人游戏,多人Ui,四个地图,四个道具,有练习模式等等。
源码已上传 (github)
UnityVersion : 2020.3.19f1c2.
主要重点
1.根本玩法填涂色块之色块衔接
需求分析:色块衔接的流动性,加分以及减去其他玩家分数,角色自己色块衔接的同时影响其他角色的色块
为了更好的实现目标设计了以下程序和结构
声明一个父类
BasicPlayer静态字典public static Dictionary<PlayerFlag, TileBase[][]> playTile;键为角色的枚举PlayerFlag,值为每一个子类所特有的用来保存瓦片的交错数组public TileBase[][] Tiles;色块一共有七种,考虑方向大概有每名角色有20不同色块
- 直接上图


大致代码
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
87public 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 | IEnumerator CreatePig(bool IsRight) |
- 冰图 冰面滑步效果,玩家进入冰面Trigger切换其移动方式为刚体施力。
1 | if(other.CompareTag("Icy")) |
- 糖图 就是简单的随机数生成,如果该
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
7speed = 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18private 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
8private 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);
}
观察者模式和单例模式的应用
单例脚本
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;
......
}(单例虽好,但不要滥用,有时候记得清理单例对象的变量,同时时时刻刻清楚其生命周期,如同对待静态变量对待他)
数据库
用于保存玩家获胜,获胜局数,最高分,总分等信息
场景异步加载以及它的加载完成后的委托事件应用
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)
{
......
}练习模式
首先添加附加场景
1
SceneManager.LoadScene("BlankTest", LoadSceneMode.Additive);
练习场景里的Camera设
Targic设置为Renderer Texture与Ui上的rawimage对应就好