大家好,我又来了。

废话不多说,咱们赶紧的,接着上一篇文章把这个联网项目搞完。

客户端发送消息

然后在NetworkClient中提供发送消息的方法,发送消息使用消息队列的机制(就是把给发送的消息放进一个队列(Queue) 通过一个协程专门向服务器发送队列中的消息)。需要发送什么消息只需往消息队列中添加消息即可。下面是封装消息数据包方法:

/// <summary>

/// 加入消息队列

/// </summary>

public static void Enqueue(MessageType type, byte[] data = null)

{

byte[] bytes = _Pack(type, data); // Pack方法在上文中已经实现

if (_curState == ClientState.Connected)

{

//加入队列

_messages.Enqueue();

}

}

以下是发送协程:

private static IEnumerator _Send()

{

//持续发送消息

while (_curState == ClientState.Connected)

{

_timer += Time.deltaTime;

//有待发送消息

if (_messages.Count > 0)

{

byte[] data = _messages.Dequeue();

yield return _Write(data); //稍后会实现

}

//心跳包机制(每隔一段时间向服务器发送心跳包)

if (_timer >= HEARTBEAT_TIME)

{

//如果没有收到上一次发心跳包的回复

if (!Received)

{

_curState = ClientState.None;

Debug.Log("心跳包接受失败,断开连接");

yield break;

}

_timer = 0;

//封装消息

byte[] data = _Pack(MessageType.HeartBeat);

//发送消息

yield return _Write(data);

Debug.Log("已发送心跳包");

}

yield return null; //防止死循环

}

}

然后就是NetworkClient关键的发送信息方法Write:

private static IEnumerator _Write(byte[] data)

{

//如果服务器下线, 客户端依然会继续发消息

if (_curState != ClientState.Connected || _stream == null)

{

Debug.Log("断开连接");

yield break;

}

//异步发送消息

IAsyncResult async = _stream.BeginWrite(data, 0, data.Length, null, null);

while (!async.IsCompleted)

{

yield return null;

}

//异常处理

try

{

_stream.EndWrite(async);

}

catch (Exception ex)

{

_curState = ClientState.None;

Debug.Log("断开连接" + ex.Message);

}

}

OK,客户端的大坑终于快填完(其实还不到一半)。
在Network中实现一个发送创建房间请求的示例(发送一般是接受用户操作后,所以这个方法可以绑定在UI上,由UI事件去触发),当然为了方便测试,放在Start方法里也没问题:

public void CreatRoomRequest(int roomId)

{

CreatRoom request = new CreatRoom();

request.RoomId = roomId;

byte[] data = NetworkUtils.Serialize(request);

NetworkClient.Enqueue(MessageType.CreatRoom, data);

}

现在,我们的客户端只会发送消息,服务器只会接收消息,简直就是一个聋子跟一个哑巴。

来点轻松点的,我们先把客户端的GamePlay部分实现吧。

客户端

基本游戏逻辑

棋盘

制作棋盘的时候,先把一张Sprite图片放进Unity场景中。为了方便我们用射线检测我们的鼠标在棋盘上的落点,我们可以在棋盘的Layer中添加上ChessBoard并且在棋盘上添加BoxCollider:

在左上角,左上角,右上角都添加好锚点
添加完锚点之后,棋盘的制作就已经完成,然后找两个适合的棋子图片做成预制体就OK了。

然后我们应该想,如何去完善棋盘的数据结构,创造一个保存所有落点世界坐标的二维数组。

一个比较好的做法是:利用这三个锚点,求出棋盘左右的宽度与上下的高度,进而可以求出每一个方格的宽度与高度。然后我们再根据左下角的锚点(原点),用一个双重循环遍历这个保存落点世界坐标的二维数组并进行赋值。以下是实现代码:

using UnityEngine;

using Multiplay;  //为协议的命名空间

/// <summary>

/// 处理下棋逻辑

/// </summary>

public class NetworkGameplay : MonoBehaviour

{

//单例

private NetworkGameplay() { }

public static NetworkGameplay Instance { get; private set; }

[SerializeField]

private GameObject _blackChess;                    //需要实例化的黑棋

[SerializeField]

private GameObject _whiteChess;                    //需要实例化的白棋

//棋盘上的锚点

[SerializeField]

private GameObject _leftTop;                       //左上

[SerializeField]

private GameObject _leftBottom;                    //左下

[SerializeField]

private GameObject _rightTop;                      //右上

private Vector2[,] _chessPos;                      //储存棋子世界坐标

private float _gridWidth;                          //网格宽度

private float _gridHeight;                         //网格高度

private void Awake()

{

if (Instance == null)

Instance = this;

_chessPos = new Vector2[15, 15];

Vector3 leftTop = _leftTop.transform.position;

Vector3 leftBottom = _leftBottom.transform.position;

Vector3 rightTop = _rightTop.transform.position;

//初始化每一个格子(一共14个)的宽度与高度

_gridWidth = (rightTop.x - leftTop.x) / 14;

_gridHeight = (leftTop.y - leftBottom.y) / 14;

//初始化每个下棋点的位置

for (int i = 0; i < 15; i++)

{

for (int j = 0; j < 15; j++)

{

_chessPos[i, j] = new Vector2

(

leftBottom.x + _gridWidth * i,

leftBottom.y + _gridHeight * j

);

}

}

}

}

OK,有了棋盘,接下来当然就是在棋盘类中提供用户输入检测接口与实例化棋子的接口。

在这里提供一个Vec2类型,方便表示棋子在棋盘上的下标:

public struct Vec2

{

public int X;

public int Y;

public Vec2(int x, int y)

{

X = x;

Y = y;

}

}

检测用户输入

/// <summary>

/// 下棋

/// </summary>

public Vec2 PlayChess()

{

//创建射线

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

RaycastHit hit;

//如果用户点中棋盘

if (Physics.Raycast(ray, out hit, 100, 1 << LayerMask.NameToLayer("ChessBoard")))

{

//遍历棋盘

for (int i = 0; i < 15; i++)

{

for (int j = 0; j < 15; j++)

{

//计算鼠标点击点与下棋点的距离(只算x,y平面距离)

float distance = _Distance(hit.point, _chessPos[i, j]);

//鼠标点击在落点周围半个格子宽度就算下棋

if (distance < (_gridWidth / 2))

{

//返回这个落点在二维数组中的下标

return new Vec2(i, j);

}

}

}

}

//未点击到棋盘

return new Vec2(-1, -1);

}

/// <summary>

/// 计算两个Vector2的距离

/// </summary>

private float _Distance(Vector2 a, Vector2 b)

{

Vector2 distance = b - a;

return distance.magnitude;

}

实例化棋子

/// <summary>

/// 实例化棋子

/// </summary>

public void InstChess(Chess chess, Vec2 pos)

{

//获取该落点的世界坐标

Vector2 vec2 = _chessPos[pos.X, pos.Y];

//棋子坐标:棋子的z坐标不能与棋盘一致且必须更靠近摄像机近截面,

//不然有可能会与棋盘重叠导致棋子不可见。

Vector3 chessPos = new Vector3(vec2.x, vec2.y, -1);

if (chess == Chess.Black)

{

Instantiate(_blackChess, chessPos, Quaternion.identity);

}

else if (chess == Chess.White)

{

Instantiate(_whiteChess, chessPos, Quaternion.identity);

}

}

制作好了棋盘脚本,可以先开始制作玩家类(Player)。

以下为玩家类,挂在场景中接收用户输入并做简单的逻辑检测。

using System;

using UnityEngine;

using Multiplay;   //为协议的命名空间

/// <summary>

/// 一个游戏客户端只能存在一个网络玩家

/// </summary>

public class NetworkPlayer : MonoBehaviour

{

//单例

private NetworkPlayer() { }

public static NetworkPlayer Instance { get; private set; }

[HideInInspector]

public Chess Chess;                     //棋子类型

[HideInInspector]

public int RoomId = 0;                  //房间号码

[HideInInspector]

public bool Playing = false;            //正在游戏

[HideInInspector]

public string Name;                     //名字

private void Awake()

{

if (Instance == null)

Instance = this;

}

private void Update()

{

if (Input.GetMouseButtonDown(0) && Playing)

{

//发送下棋请求 TODO

}

}

}

在这里提供个客户端的类型,方便之后开发。

Info类型作用于UI,可以在某个Text上显示信息:

using UnityEngine;

using UnityEngine.UI;

public class Info : MonoBehaviour

{

private Info() { }

public static Info Instance { get; private set; }

private Text _text;

private void Awake()

{

if (Instance == null)

Instance = this;

_text = GetComponent<Text>();

}

/// <summary>

/// 打印

/// </summary>

public void Print(string str, bool warning = false)

{

if (warning)

Debug.LogWarning(str);

else

Debug.Log(str);

_text.text = str;

}

}

至于游戏的UI部分,此处不做详细介绍。

以下是本游戏UI提供的用户接口,仅供参考:

[SerializeField]

private InputField _ipAddressIpt;     //服务器IP输入框

[SerializeField]

private InputField _roomIdIpt;        //房间号码输入框

[SerializeField]

private InputField _nameIpt;          //名字输入框

[SerializeField]

private Button _connectServerBtn;     //连接服务器按钮

[SerializeField]

private Button _enrollBtn;            //注册按钮

[SerializeField]

private Button _creatRoomBtn;         //创建房间按钮

[SerializeField]

private Button _enterRoomBtn;         //加入房间按钮

[SerializeField]

private Button _exitRoomBtn;          //退出房间按钮

[SerializeField]

private Button _startGameBtn;         //开始游戏按钮

[SerializeField]

private Text _gameStateTxt;           //游戏状态文本

[SerializeField]

private Text _roomIdTxt;              //房间号码文本

[SerializeField]

private Text _nameTxt;                //名字文本

private void Start()

{

//绑定按钮事件

_connectServerBtn.onClick.AddListener(_ConnectServerBtn);

_enrollBtn.onClick.AddListener(_EnrollBtn);

_creatRoomBtn.onClick.AddListener(_CreatRoomBtn);

_enterRoomBtn.onClick.AddListener(_EnterRoomBtn);

_exitRoomBtn.onClick.AddListener(_ExitRoomBtn);

_startGameBtn.onClick.AddListener(_StartGameBtn);

}

实现了基本的客户端棋盘逻辑。接下来就是重点了,涉及到客户端与服务器的网络通讯部分。现在我们的客户端已经具备接受玩家输入,并且可以把用户鼠标点击的位置转化为棋盘上的位置。

客户端接受消息

有了发送消息肯定少不了接受消息,客户端必须对服务器发来的反馈再进行操作。

接收消息这里有个也有回调事件机制:在把数据包拆成:消息长度,消息类型之后,我们可以通过不同的消息类型去执行不同的回调方法。

以下为NetworkClient的接收消息(Receive)方法的关键代码:

while(true)

{

//解析数据包过程(服务器与客户端需要严格按照一定的协议制定数据包)

byte[] data = new byte[4]; //数据包包头长度(2 + 2)

int length;         //消息总长度

MessageType type;   //类型

int receive = 0;    //接收到的数据长度

//异步读取

IAsyncResult async = _stream.BeginRead(data, 0, data.Length, null, null);

while (!async.IsCompleted)

{

yield return null;

}

//异步读取完毕

receive = _stream.EndRead(async);

//解析包头

using (MemoryStream stream = new MemoryStream(data))

{

BinaryReader binary = new BinaryReader(stream, Encoding.UTF8); //UTF-8格式

length = binary.ReadUInt16();

type = (MessageType)binary.ReadUInt16();

}

//如果有包体

if (length - 4 > 0)

{

data = new byte[length - 4];

//异步读取

async = _stream.BeginRead(data, 0, data.Length, null, null);

while (!async.IsCompleted)

{

yield return null;

}

//异步读取完毕

receive = _stream.EndRead(async);

}

//没有包体

else

{

data = new byte[0];

receive = 0;

}

//反序列化回消息类型

CreatRoom result = NetworkUtils.Deserialize<CreatRoom>(data);

}

回调事件对客户端做的具体操作这里就不放源码了,只示范一个事件:

private void _Heartbeat(byte[] data)

{

NetworkClient.Received = true;

Debug.Log("收到心跳包回应");

}

与服务器类似,回调的核心就是把消息类型与相对应的回调事件一起注册一个字典(这个过程在客户端接收服务器数据之前)。

每次接受消息后,只需要把这次的消息类型与字典中进行匹配,进而客户端执行相对应的回调事件即可。以下为回调机制关键代码:

//注册回调事件

public static void Register(MessageType type, CallBack method)

{

if (!_callBacks.ContainsKey(type))

_callBacks.Add(type, method);

else

Debug.LogWarning("注册了相同的回调事件");

}

//执行回调,这里的代码应该放在接收消息方法中

if (_callBacks.ContainsKey(type))

{

//执行回调事件

CallBack method = _callBacks[type];

method(data);

}

到这里,客户端的关键制作思路已经介绍完成。

服务器发送消息

以下是服务器对客户端的发送消息的方法,下面是代码:

/// <summary>

/// 封装并发送信息 ,写在Server中的Player类型扩展方法

/// </summary>

public static void Send(this Player player, MessageType type, byte[] data = null)

{

byte[] bytes = _Send(type, data); //在介绍数据包时已经实现

//发送消息

player.Socket.Send(bytes);

}

终于不是两个残疾人在通信了。
服务器房间系统

接下来还有房间类型,房间构成了一局游戏的基础。

在本游戏中,房间号码不可重复。当一个玩家创建好一个房间时,其他玩家在玩家人数未满时加入房间,就会成为玩家。在玩家人数满了,但是观察者人数未满时加入房间就是观察者。

如果全部满了之后就无法进入该房间。当一个房间的状态进入开始游戏状态后,所有人都无法再进入其中。当一局游戏结束后,此房间会自动关闭。

然后我们在服务器的Room类中添加一个方法:

/// <summary>

/// 关闭房间:从房间字典中移除并且所有房间中的玩家清除

/// </summary>

public void Close()

{

//所有玩家跟观战者退出房间

foreach (var each in Players)

{

each.ExitRoom();

}

foreach (var each in OBs)

{

each.ExitRoom();

}

Server.Rooms.Remove(RoomId);

}

由于服务器回调事件较多,只展示创建心跳包回调事件的关键代码:

private void _HeartBeat(Player player, byte[] data)

{

//仅做回应

player.Send(MessageType.HeartBeat);

}

服务器核心逻辑实现

服务器可以存在多个不同房间号的房间,每个房间对应一个棋盘,每个房间上还拥有多个玩家。按照这个思路再去设计棋盘的逻辑。

以下为棋盘的关键代码:

/// <summary>

/// 初始化棋盘

/// </summary>

public GamePlay()

{

ChessState = new Chess[15, 15];

_totalChess = 0;

Playing = true;

Turn = Chess.Black;

}

public Chess[,] ChessState;                     //储存棋子状态

private int _totalChess;                        //总棋数

public bool Playing;                            //游戏进行中

public Chess Turn;                              //轮流下棋

游戏逻辑实现

服务器上的这个棋盘才是真正进行五子棋逻辑操作的棋盘,关键的算法都在上面实现。客户端每发送一次下棋操作,都会在棋盘上进行计算,结果再由服务器广播给在这个房间中的所有人,包括玩家们与观察者们。

以下为五子棋玩法逻辑的算法:

public Chess Calculate(int x, int y)

{

if (!Playing) return Chess.Null;

//逻辑判断

if (x < 0 || x >= 15 || y < 0 || y >= 15 || ChessState[x, y] != Chess.None)

{

return Chess.Null;

}

//下棋

_totalChess++;

//黑棋

if (Turn == Chess.Black)

{

ChessState[x, y] = Chess.Black;

}

//白棋

else if (Turn == Chess.White)

{

ChessState[x, y] = Chess.White;

}

//计算结果

bool? result = _CheckWinner();

//要么平局要么胜利(任意一方胜利后不在交替下棋,游戏结束)

if (result != false)

{

//游戏结束

Playing = false;

//胜利

if (result == true)

{

return Turn;

}

//平局

else

{

return Chess.Draw;

}

}

//继续下棋

else

{

//交替下棋

Turn = (Turn == Chess.Black ? Chess.White : Chess.Black);

return Chess.None;

}

}

private bool? _CheckWinner()

{

//遍历棋盘

for (int i = 0; i < 15; i++)

{

for (int j = 0; j < 15; j++)

{

//各方向连线

int horizontal = 1, vertical = 1, rightUp = 1, rightDown = 1;

Chess curPos = ChessState[i, j];

if (curPos != Turn)

continue;

//判断5连

for (int link = 1; link < 5; link++)

{

//扫描横线

if (i + link < 15)

{

if (curPos == ChessState[i + link, j])

horizontal++;

}

//扫描竖线

if (j + link < 15)

{

if (curPos == ChessState[i, j + link])

vertical++;

}

//扫描右上斜线

if (i + link < 15 && j + link < 15)

{

if (curPos == ChessState[i + link, j + link])

rightUp++;

}

//扫描右下斜线

if (i + link < 15 && j - link >= 0)

{

if (curPos == ChessState[i + link, j - link])

rightDown++;

}

}

//胜负判断

if (horizontal == 5 || vertical == 5 || rightUp == 5 || rightDown == 5)

{

return true;

}

}

}

//棋盘下满

if (_totalChess == ChessState.GetLength(0) * ChessState.GetLength(1))

{

//平局

return null;

}

return false;

}

那么,对于服务器来说,还有一件重要的事情,如何把一个玩家的操作发送给相同房间里的所有玩家与观察者呢?这个可以通过对房间内保存的每一个玩家的套接字(Socket)进行发送数据操作。

以下为广播给所有玩家的关键代码:

//判断结果

Chess chess = Server.Rooms[receive.RoomId].GamePlay.Calculate(receive.X, receive.Y);

//检测操作:如果游戏结束

bool over = _ChessResult(chess, result);

PlayChess result = new PlayChess();

result.Chess = receive.Chess;

result.X = receive.X;

result.Y = receive.Y;

Console.WriteLine($"玩家:{player.Name}下棋成功");

//向该房间中玩家与观察者广播结果

data = NetworkUtils.Serialize(result);

foreach (var each in Server.Rooms[receive.RoomId].Players)

{

each.Send(MessageType.PlayChess, data);

}

foreach (var each in Server.Rooms[receive.RoomId].OBs)

{

each.Send(MessageType.PlayChess, data);

}

还有很多服务器对房间系统所做的逻辑处理都写在服务器的回调事件中,因为代码量较多不能一一列出。更多细节会在文章结尾放出源码,欢迎学习或吐槽。

好了。到此为止,恭喜你已经达成“从无到有制作局域网联机游戏”这样一个成就。过程中的代码量和经验有没有给你一种脱胎换骨的赶脚?

完整工程如下:https://pan.baidu.com/s/19rfHgHZIe55dZ7LVIL1oUg?errno=0&errmsg=Auth%20Login%20Sucess&&bduss=&ssnerror=0&traceid=

OK,希望本文对在游戏开发的道路上的你有所启发。

我们来用Unity做一个局域网游戏(下)相关推荐

  1. unity做一个小游戏(适合零基础或者巩固加深unity中的工具类的用法)

    今天跟着官方unity做了一个小游戏.巩固一下之前学习的unity的知识.注意unity的版本要在2018.3以上 大概游戏是这样子的如图:人物只能控制左右移动,空格发射饼干,动物从屏幕上方随机出现在 ...

  2. 用Unity做一个萌萌哒游戏(附资源)

    这第一期是个考反应的游戏,看看效果图便能瞬间明白主要玩法: 首先需要准备游戏素材.对喜欢亲自动(zhe)手(teng)的我来说不是个事儿,我使用了PhotoShop+数位板自行绘制.没有数位板的同学只 ...

  3. 用unity 做一个转盘游戏

    此游戏为课堂小测验,下面为大家分享代码思路 本转盘为转盘动,中间指针不动,不过指针动也是和此demo大同小异 下面为示意图 下面为全部代码(有详细注释) using System.Collection ...

  4. scratch做简单跑酷游戏_腾讯游戏学院专家:做一个多线程游戏框架可以多简单?...

    导语 如何做一个多线程游戏框架?腾讯游戏学院专家Tao将在本文通过一个demo来说说游戏逻辑的多线程化. 众所周知现在各种游戏终端的发展十分迅猛.其中一个共同的特征是"多核化",由 ...

  5. 「Unity2D」使用Unity创建一个2D游戏系列-1

    「Unity2D」使用Unity创建一个2D游戏系列-1 安装unity并且创建你的第一个场景 在第一章,你将会学习到一些非常基本的内容:首先是unity的下载和安装,其次是准备创建我们游戏内的第一个 ...

  6. 用 JS 做一个数独游戏(二)

    用 JS 做一个数独游戏(二) 在 上一篇博客 中,我们通过 Node 运行了我们的 JavaScript 代码,在控制台中打印出来生成好的数独终盘.为了让我们的数独游戏能有良好的体验,这篇博客将会为 ...

  7. python手机版做小游戏代码大全-Python大牛手把手教你做一个小游戏,萌新福利!...

    原标题:Python大牛手把手教你做一个小游戏,萌新福利! 引言 最近python语言大火,除了在科学计算领域python有用武之地之外,在游戏.后台等方面,python也大放异彩,本篇博文将按照正规 ...

  8. 做一个FLASH游戏你需要掌握的东西【实用】

    做一个FLASH游戏你需要掌握的东西 作者:jianzhong 一直想着什么时间好好做一个像样点的游戏,于是刻意的开始去了解FLASHGAME的相关资料,在这里把自己在整个制作和收集过程中的一些感觉使 ...

  9. 零基础Unity做一个中秋诗词鉴赏网页,提前祝您中秋快乐!(DoTween动画 | WebGL视频 | 大文件上传GitHub)

    零基础Unity做一个中秋诗词鉴赏网页,提前祝您中秋快乐! 前言 一,环境搭建 1.1 安装Unity 1.2 添加WebGl模块 二,开发项目 2.1 导入插件 2.2 项目搭建 2.3 逻辑处理 ...

最新文章

  1. 某知名大学学生毕业设计,Java学好了就是厉害
  2. python 动漫卡通人物图片大全_用Python把人物头像动漫化,不同的表情给你不同的惊喜...
  3. 深度学习在物理层信号处理中的应用研究
  4. JavaScript可变参数个数
  5. python能做什么-学 Python 都用来干嘛的?
  6. python爬虫毕业论文大纲参考模板_毕业论文大纲参考模板
  7. 安装VMware15.5+安装win10虚拟机操作系统
  8. CSP201903-1 小中大 (Python)
  9. php 生成pdf 中文,用PHP创建PDF中文文档
  10. 小米手机访问电脑共享文件_小米手机共享文件夹在哪里
  11. vuejs出的手机app有哪些_vue.js点餐app手机触屏滑动分类菜单切换代码
  12. fps透视基础-3分钟快速定位矩阵基址-附3D坐标转屏幕坐标算法
  13. android wms各个类的作用,Android系统服务 —— WMS
  14. NFC:跟现金和信用卡说不
  15. 新手如何做游戏代理赚钱?
  16. 用尘埃粒子计数器对高效过滤器检测检漏方法怎样?
  17. 前程无忧呼吁监管ATS(招聘管理平台)、举报非法社群简历买卖
  18. C语言花样霓虹灯程序,LM4229显示屏的单片机按键控制多种花样霓虹灯设计报告与源码...
  19. word 制表位之mythtype公式编号右对齐
  20. wma转换mp3格式怎么转?

热门文章

  1. 众人逃离北上广后又逃回:观念不合拍还要拼爹
  2. 音视频基础:音频(PCM和AAC)
  3. 【工具】Excel表格数据不能编辑
  4. 大学生简单抗击疫情静态HTML网页设计作品 DIV布局疫情感动人物介绍网页模板代码 DW学生抗疫逆行者网站制作成品下载
  5. linux设置spi时钟频率,Linux下S3C2416的SPI设置问题,CLK和MOSI都没有输出,求助
  6. uniapp实现app跳转app
  7. 【面试专栏】第三篇:Java基础:集合篇-List、Queue
  8. 网页设计中分栏布局的几种实现方案
  9. vue + gifshot 实现GIF动图
  10. DDOS防护如何建设?