您现在的位置是:首页 >技术杂谈 >Unity 简单联网游戏(双人五子棋)开发(二)网站首页技术杂谈
Unity 简单联网游戏(双人五子棋)开发(二)
前言:之前我们尝试开发了一个两个比拼分数的不像游戏的超简单的弱数据联网游戏,主要是想让一些没开发过联网游戏的人了解一下最基础的流程;不过有人仍然有人私信我表示看不懂,所以这次我们再开发一个类似的游戏,为了这个项目更像是一个游戏,而不是不像游戏的游戏,所以选择开发这个双人五子棋项目。
需求分析:
五子棋的规则对战规则大家都懂,我就不在这扯了,我们就只需要实现下棋,然后判断输赢就行,联网部分我们还是使用LeanCloud,好了,我们直接开始。
双人五子棋
1.准备素材
这是我找的素材,需要的自提。
2.创建项目
Canvas里面的可以按照我这样弄,startBtn就是左上的开始按钮,Ts是右下的提示,暂时没用到,black和white是左下的黑白棋子,是用了作预制体使用的,放在上面,是为了让其大小随棋盘而变。
为了不让由于分辨率的原因而影响到下棋的位子误差,同时减少计算,棋盘高度就是Screen的高度,为了保证棋盘不应拉伸缩小而变形,我们需要一下操作
3.sdk
sdk需要去leancloud或github官网下载,这个是免费的,放心下载,然后整成我这样就行
嫌github慢可以用我的。
链接: https://pan.baidu.com/s/1ysFdiLd1kkFliu_stbHHVA?pwd=qwer 提取码: qwer
4.脚本
Game.cs
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using LeanCloud.Play;
public class Game : BasePanel, IPointerClickHandler
{
public GameObject black;
public GameObject white;
private Button startBtn;
private float grid;
private Chess[,] chess;
private bool yourTurn;
private bool isBlack;//是否执黑,执黑先行
private bool gamOver;
private bool youWon;
public class Chess
{
public Vector2 chessPos;//格子位置
public int chessState = 0;//格子状态 1黑 -1白 0空
}
public class Pos
{
public int x;
public int y;
}
Pos Dis(Vector2 pos)
{
float dis = 10000000000;
Pos p = new Pos();
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
if ((chess[i, j].chessPos - pos).sqrMagnitude < dis)
{
dis = (chess[i, j].chessPos - pos).sqrMagnitude;
p.x = i; p.y = j;
}
}
}
if (dis > grid * grid / 2)
{
return null;
}
else
{
return p;
}
}
protected override void Awake()
{
base.Register();
grid = Screen.height * 11 / 168;
Vector2 LTPos = new Vector2(transform.position.x - grid * 7, Screen.height * 23 / 24);
startBtn = GetControl<Button>("startBtn");
startBtn.gameObject.SetActive(false);
chess = new Chess[15, 15];
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
chess[i, j] = new Chess();
chess[i, j].chessPos = new Vector2(LTPos.x + i * grid, LTPos.y - j * grid);
chess[i, j].chessState = 0;
}
}
gamOver = false;
youWon = false;
}
/// <summary>
/// 自己下棋
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
if (eventData.pointerClick && yourTurn)
{
Pos p = Dis(eventData.position);
if (p == null) return;
// var dis=(eventData.po)
if (chess[p.x, p.y].chessState == 0)
{
yourTurn = false;
GameObject go;
if (isBlack)
{
chess[p.x, p.y].chessState = 1;
go = Instantiate(black, transform);
}
else
{
chess[p.x, p.y].chessState = -1;
go = Instantiate(white, transform);
}
go.transform.position = chess[p.x, p.y].chessPos;
GameOver(p);
}
}
//可以再这里模拟对手下棋进行测试一下
}
/// <summary>
/// 对手下棋
/// </summary>
public void RivalPoninterClick(int x, int y)
{
GameObject go;
if (isBlack)
{
chess[x, y].chessState = -1;
go = Instantiate(white, transform);
}
else
{
chess[x, y].chessState = 1;
go = Instantiate(black, transform);
}
go.transform.position = chess[x, y].chessPos;
GameOver(new Pos() { x = x, y = y });
}
/// <summary>
/// 判断输赢
/// </summary>
private void GameOver(Pos p)
{
int tempXIndex = p.x;
int tempYIndex = p.y;
int[,,] dir = new int[4, 2, 2] {
{ { -1, 0 }, { 1, 0 } },
{ { 0, -1 }, { 0, 1 } },
{ { -1, -1 }, { 1, 1 } },
{ { 1, -1 }, { -1, 1 } } };
for (int i = 0; i < 4; i++)
{
int count = 1;
for (int j = 0; j < 2; j++)
{
while (true)
{
tempXIndex = tempXIndex + dir[i, j, 0];
tempYIndex = tempYIndex + dir[i, j, 1];
if (isBorder(tempXIndex, tempYIndex) && chess[tempXIndex, tempYIndex].chessState == chess[p.x, p.y].chessState)
{
count++;
}
else break;
}
tempXIndex = p.x;
tempYIndex = p.y;
}
if (count >= 5 && chess[tempXIndex, tempYIndex].chessState == 1)
{
gamOver = true;
if (isBlack)
{
youWon = true;
}
}
if (count >= 5 && chess[tempXIndex, tempYIndex].chessState == -1)
{
gamOver = true;
if (!isBlack)
{
youWon = true;
}
}
}
}
private bool isBorder(int x, int y)
{
if (x >= 0 && x < 15 && y >= 0 && y < 15)
{
return true;
}
else return false;
}
}
}
其实这个脚本可以先不用管,后面还会往里面添加联网部分,至于继承的BasePanel,主要为了省事而用的自己常用的小小框架,不喜欢的话可以自己把game脚本的获取UI的改成常规的就行
BasePanel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class BasePanel : MonoBehaviour
{
public Dictionary<string, List<UIBehaviour>> controlDic = new Dictionary<string, List<UIBehaviour>>();
private bool active = true;
public bool Active
{
get
{
return active;
}
set
{
if (active != value)
{
active = value;
gameObject.SetActive(value);
}
}
}
protected virtual void Register()
{
FindControl<Button>();
FindControl<Text>();
FindControl<Image>();
}
protected virtual void Awake()
{
Register();
//WindowsMgr.Instance.windows.Add(this.name, this);//添加到窗体管理字典
Active = false;//默认隐藏
}
/// <summary>
/// 获取UI
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="controlName"></param>
/// <returns></returns>
protected T GetControl<T>(string controlName) where T : UIBehaviour
{
if (controlDic.ContainsKey(controlName))
{
for (int i = 0; i < controlDic[controlName].Count; i++)
{
if (controlDic[controlName][i] is T)
{
return controlDic[controlName][i] as T;
}
}
}
return null;
}
/// <summary>
/// 将UI添加到列表
/// </summary>
/// <typeparam name="T"></typeparam>
protected void FindControl<T>() where T : UIBehaviour
{
T[] control = this.GetComponentsInChildren<T>(true);
string controlName;
for (int i = 0; i < control.Length; i++)
{
controlName = control[i].gameObject.name;
if (controlDic.ContainsKey(controlName))
{
controlDic[controlName].Add(control[i]);
}
else
{
controlDic.Add(controlName, new List<UIBehaviour> { control[i] });
}
}
}
}
至此,非联网部分的已经完成了。
接下来就是将联网功能的实现,先将代码拆出来解释下吧。
const byte GAME_OVER_EVENT = 100;
const byte GAME_START_EVENT = 101;
const byte HEAD_START_EVENT = 102;
这东西在Android里面应该叫做请求码,就是一个标识,需要啥的就自己定义。
var appID = "************";
var appKey = "******************";
var playServer = "https://************";//你的API服务器地址
这是都是接sdk必须要有的,在leancud创建自己的应用以后可以在设置中找到。
var now = System.DateTime.Now;
var roomName = $"{now.Hour}";
var options = new RoomOptions()
{
EmptyRoomTtl = 10,
MaxPlayerCount = 2,
Flag = CreateRoomFlag.FixedMaster
};
var res = client.JoinOrCreateRoom(roomName);
await res;
while (res.Exception != null)
{
roomName = roomName + now.Hour;
await res;
}
这是创建或加入房间。
其他的就不说了,代码中有注释,应该都能看懂。
修改后的Game.cs
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using LeanCloud.Play;
public class Game : BasePanel, IPointerClickHandler
{
public GameObject black;
public GameObject white;
private Button startBtn;
private float grid;
private Chess[,] chess;
private bool yourTurn;
private bool isBlack;//是否执黑,执黑先行
private bool gamOver;
private bool youWon;
public class Chess
{
public Vector2 chessPos;//格子位置
public int chessState = 0;//格子状态 1黑 -1白 0空
}
public class Pos
{
public int x;
public int y;
}
Pos Dis(Vector2 pos)
{
float dis = 10000000000;
Pos p = new Pos();
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
if ((chess[i, j].chessPos - pos).sqrMagnitude < dis)
{
dis = (chess[i, j].chessPos - pos).sqrMagnitude;
p.x = i; p.y = j;
}
}
}
if (dis > grid * grid / 2)
{
return null;
}
else
{
return p;
}
}
protected override void Awake()
{
base.Register();
grid = Screen.height * 11 / 168;
Vector2 LTPos = new Vector2(transform.position.x - grid * 7, Screen.height * 23 / 24);
// Debug.Log(transform.GetComponent<RectTransform>().rect.y);
// Vector2 RTPos = GetControl<Image>("RTPos").transform.position;
// Vector2 LBPos = GetControl<Image>("LBPos").transform.position;
//Debug.Log($"{transform.position.x} { transform.position.y}");
//Debug.Log(grid);
// float gridWidth = (RTPos.x - LTPos.x) / 14;
// float gridHeight = (LTPos.y - LBPos.y) / 14;
startBtn = GetControl<Button>("startBtn");
startBtn.gameObject.SetActive(false);
chess = new Chess[15, 15];
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
chess[i, j] = new Chess();
chess[i, j].chessPos = new Vector2(LTPos.x + i * grid, LTPos.y - j * grid);
chess[i, j].chessState = 0;
}
}
gamOver = false;
youWon = false;
//isBlack = true;
//RivalPoninterClick(0, 0);
//RivalPoninterClick(14, 0);
//RivalPoninterClick(14, 14);
//StartCoroutine("Tips");
}
IEnumerator Tips()
{
yield return new WaitForSeconds(0.2f);
Pos pos = new Pos();
while (true)
{
if (gamOver)
{
break;
}
if (yourTurn)
{
yield return new WaitForSeconds(0.2f);
var dis = Dis(Input.mousePosition);
bool same = pos == dis ? true : false;
if (!same)
{
if (chess[dis.x, dis.y].chessState == 0)
{
GetControl<Image>("Ts").transform.position = chess[pos.x, pos.y].chessPos;
GetControl<Image>("Ts").enabled = true;
pos = dis;
}
else
{
GetControl<Image>("Ts").transform.position = chess[pos.x, pos.y].chessPos;
GetControl<Image>("Ts").enabled = false;
}
}
}
}
}
/// <summary>
/// 自己下棋
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
if (eventData.pointerClick && yourTurn)
{
Pos p = Dis(eventData.position);
if (p == null) return;
// var dis=(eventData.po)
if (chess[p.x, p.y].chessState == 0)
{
yourTurn = false;
var playerList = client.Room.PlayerList;
for (int i = 0; i < playerList.Count; i++)
{
var player = playerList[i];
if (player.IsLocal)
{
var props = player.CustomProperties;
props["px"] = p.x;
props["py"] = p.y;
player.SetCustomProperties(props);
}
}
GameObject go;
if (isBlack)
{
chess[p.x, p.y].chessState = 1;
go = Instantiate(black, transform);
}
else
{
chess[p.x, p.y].chessState = -1;
go = Instantiate(white, transform);
}
go.transform.position = chess[p.x, p.y].chessPos;
GameOver(p);
}
}
}
/// <summary>
/// 对手下棋
/// </summary>
public void RivalPoninterClick(int x, int y)
{
GameObject go;
if (isBlack)
{
chess[x, y].chessState = -1;
go = Instantiate(white, transform);
}
else
{
chess[x, y].chessState = 1;
go = Instantiate(black, transform);
}
go.transform.position = chess[x, y].chessPos;
GameOver(new Pos() { x = x, y = y });
}
/// <summary>
/// 判断输赢
/// </summary>
private void GameOver(Pos p)
{
int tempXIndex = p.x;
int tempYIndex = p.y;
int[,,] dir = new int[4, 2, 2] {
{ { -1, 0 }, { 1, 0 } },
{ { 0, -1 }, { 0, 1 } },
{ { -1, -1 }, { 1, 1 } },
{ { 1, -1 }, { -1, 1 } } };
for (int i = 0; i < 4; i++)
{
int count = 1;
for (int j = 0; j < 2; j++)
{
while (true)
{
tempXIndex = tempXIndex + dir[i, j, 0];
tempYIndex = tempYIndex + dir[i, j, 1];
if (isBorder(tempXIndex, tempYIndex) && chess[tempXIndex, tempYIndex].chessState == chess[p.x, p.y].chessState)
{
count++;
}
else break;
}
tempXIndex = p.x;
tempYIndex = p.y;
}
if (count >= 5 && chess[tempXIndex, tempYIndex].chessState == 1)
{
gamOver = true;
if (isBlack)
{
youWon = true;
}
}
if (count >= 5 && chess[tempXIndex, tempYIndex].chessState == -1)
{
gamOver = true;
if (!isBlack)
{
youWon = true;
}
}
}
}
private bool isBorder(int x, int y)
{
if (x >= 0 && x < 15 && y >= 0 && y < 15)
{
return true;
}
else return false;
}
//---------------------------------------------------------------------------------------------------------
const byte GAME_OVER_EVENT = 100;
const byte GAME_START_EVENT = 101;
const byte HEAD_START_EVENT = 102;
// 获取客户端 SDK 实例
private Client client;
private async void Start()
{
var appID = "*********************";
var appKey = "*******************";
var playServer = "https://*******************";
// 这里使用随机数作为 userId
var random = new System.Random();
var randId = string.Format("{0}", random.Next(10000000));
// 初始化
client = new Client(appID, appKey, randId, playServer: playServer);
await client.Connect();
// 根据当前时间(时,分)生成房间名称
var now = System.DateTime.Now;
var roomName = $"{now.Hour}";
var options = new RoomOptions()
{
EmptyRoomTtl = 10,
MaxPlayerCount = 2,
Flag = CreateRoomFlag.FixedMaster
};
var res = client.JoinOrCreateRoom(roomName);
await res;
while (res.Exception != null)
{
roomName = roomName + now.Hour;
await res;
}
Debug.Log(roomName);
// 注册新玩家加入房间事件
client.OnPlayerRoomJoined += (newPlayer) =>
{
bool bo = now.Second % 2 == 0 ? true : false;
Debug.Log("匹配成功,可以开始游戏了");
if (client.Player.IsMaster)
{
Debug.Log(client.Room.PlayerList.Count);
// 获取房间内玩家列表
var data = new PlayObject { { "HeadStart", bo } };
var opts = new SendEventOptions
{
ReceiverGroup = ReceiverGroup.All
};
client.SendEvent(HEAD_START_EVENT, data, opts);
}
};
// 注册「玩家属性变更」事件
client.OnPlayerCustomPropertiesChanged += (player, changedProps) =>
{
string point = player.CustomProperties.GetString("name");
int x = player.CustomProperties.GetInt("px");
int y = player.CustomProperties.GetInt("py");
if (player.IsLocal)
{
}
else
{
//将对手下子情况显示在自己棋盘上
if (isBorder(x, y))
{
RivalPoninterClick(x, y);
if (!gamOver)
{
yourTurn = true;
}
}
}
if (youWon == true)
{
var data = new PlayObject { { "winnerId", player.ActorId } };
var opts = new SendEventOptions
{
ReceiverGroup = ReceiverGroup.All
};
client.SendEvent(GAME_OVER_EVENT, data, opts);
}
};
// 注册自定义事件
client.OnCustomEvent += (eventId, eventData, senderId) =>
{
if (eventId == HEAD_START_EVENT)
{
var HeadStart = eventData.GetBool("HeadStart");
// TODO 处理释放技能的表现
if (client.Player.IsMaster)
{
isBlack = HeadStart;
startBtn.gameObject.SetActive(HeadStart);
Debug.Log($"先手{HeadStart}");
}
else
{
isBlack = !HeadStart;
startBtn.gameObject.SetActive(!HeadStart);
}
}
else if (eventId == GAME_OVER_EVENT)
{
int winnerId = eventData.GetInt("winnerId");
if (client.Player.ActorId == winnerId)
{
GetControl<Text>("win").text = "你赢了";
}
else
{
GetControl<Text>("win").text = "你输了";
}
}
else if (eventId == GAME_START_EVENT)
{
Debug.Log("开始游戏");
startBtn.gameObject.SetActive(false);
yourTurn = isBlack;
var playerList = client.Room.PlayerList;
for (int i = 0; i < playerList.Count; i++)
{
var player = playerList[i];
var props = new PlayObject();
props.Add("name", player.UserId);
props.Add("px", 100);
props.Add("py", 100);
player.SetCustomProperties(props);
}
// StartCoroutine("Tips");
}
};
startBtn.onClick.AddListener(() =>
{
var opts = new SendEventOptions
{
ReceiverGroup = ReceiverGroup.All
};
client.SendEvent(GAME_START_EVENT, null, opts);
});
}
}
除了获取UI的BasePanel就这一个脚本,照搬就行,别忘了填上自己appID和Key以及API服务器连接地址。
6.运行结果:
就到这了,这是关于开发弱数据联网游戏的第二篇了,接下来我们提升一点点难度,尝试开发联网吃豆人游戏,有兴趣的可以留意一下。