为Unity游戏搭建SpringBoot的服务端实现多人同步

最近在研究Unity中多人游戏的实现,要保证在同一游戏里不同玩家所用的客户端之间的状态保持同步,需要配置统一的服务器来分发玩家状态的列表,以在客户端完成多人状态的更新。

做一个小的联机Demo,传输玩家的位置、水平转角,动画状态等信息,通信的逻辑是当客户端将玩家的信息更新到服务端的同时,将状态信息储存在服务端,服务端再响应给客户端当前游戏所有玩家的状态信息列表。

为什么要用Java的SpringBoot框架来搭服务器呢,是由于最近刚好学了Java和SpringBoot的一些知识,想着自己动手熟悉一下,当然如果有大佬能指出一些不足的地方就更好啦。网上找了一些SpringBoot集成WebSocket的用例之后,直接开干

先来看看效果吧:

分别介绍一下服务端和客户端的一些代码:

服务端:

SpringBoot项目的代码结构如下:

首先,在application.yml配置服务器端口:

server:
port: 8888

Pom.xml中引入相应的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
</dependency>

然后创建消息类Message和玩家信息类PlayerInfo两个实体类:

@Data
@Component
@NoArgsConstructor
@AllArgsConstructor
public class Message {
    public String type;
    public String info;
} 
@Data
@Component
@NoArgsConstructor
@AllArgsConstructor
public class PlayerInfo implements Serializable {

    public String name;
    public float x, y, z;
    public float ry; //旋转,只考虑角色的水平旋转
    public float speed; //玩家移动的速度
    public float motionSpeed; //玩家移动动画速度
    public boolean roll; //玩家是否翻滚

} 

接下来编写三层架构。主要的逻辑为,当Controller层收到客户端的数据包后,通过注解@OnMessage调用onMessage方法,将数据包交给Service层处理,然后通过Service请求返回当前所有玩家的状态列表,打包成Message再转换Json格式发送给客户端。

客户端与SpringBoot服务端的三层架构通信

Controller层是负责和前端(客户端)打交道的,为了实现简单,这里采用了WebSocket来进行信息的接发,代码如下:

@ServerEndpoint("/")
@Component
public class WebSocketServer {

    private Session session;

    private static PlayerInfoServiceImpl playerInfoServiceImpl=new PlayerInfoServiceImpl();

  	//收到消息时执行
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        System.out.println("从客户端收到的消息:" + message);
        playerInfoServiceImpl.saveInfoService(message);
        sendMessage(new Message(MsgTypeConstant.ALL_PLAYER_INFO,
                JSONArray.toJSONString(playerInfoServiceImpl.getPlayInfoList())));
    }

    public void sendMessage(Message message) throws IOException{
        this.session.getAsyncRemote().sendText(JSONArray.toJSONString(message));
    }
} 

顺着这个思路,Service层就负责两件事,一个是将收到的数据保存到数据库中的玩家状态列表,一个是将数据库中的玩家状态列表返回出来。代码如下:

public class PlayerInfoServiceImpl implements PlayerInfoService {
  	//将数据保存至玩家状态列表
    @Override
    public void saveInfoService(String message) {
        String[] msgList = message.split("&");//以&作为数据包的分割字符
        for(String _msg : msgList){
            if(_msg.length() > 0){
                Message msg = JSON.parseObject(_msg, Message.class);
                if(msg.type.equals(MsgTypeConstant.UPDATE_PLAYER_INFO)){
                    PlayerInfo thisPlayer = JSON.parseObject(msg.info, PlayerInfo.class);
                    userInfoMapper.userInfo.put(thisPlayer.name, thisPlayer);
                }
            }
        }
    }

  //从玩家状态列表获取数据
    @Override
    public HashMap<String, PlayerInfo> getPlayInfoList() {
        return userInfoMapper.userInfo;
    }
} 

Mapper层负责和数据库打交道,为了简单起见,这里就不涉及数据库的存储,先将玩家状态列表保存至Mapper层

public class userInfoMapper {

    //用于储存所有玩家的状态信息
    public static HashMap<String, PlayerInfo> userInfo = new HashMap<>();
}

ok,简单的服务端就搭建好了,当多个玩家通过这个端口访问服务器时,就可以实时同步他们的信息。如果后面时间允许,应该还得补上数据库的访问以及各种异常处理。下面来看看客户端的一些主要代码。


Unity 客户端

创建PlayerInfo.cs脚本构造玩家状态类

public class PlayerInfo
{
    public string name;
    public float x, y, z;
    public float ry; //旋转,只用考虑角色的水平旋转,节省带宽
    public float speed; //玩家移动的速度
    public float motionSpeed; //玩家移动动画速度
    public bool roll; //玩家是否翻滚

    public PlayerInfo(string n, Vector3 pos, Vector3 rot, float speed, float motionSpeed, bool roll)
    {
        name = n;
        x = pos.x; y = pos.y; z = pos.z;
        ry = rot.y;
        this.speed = speed;
        this.motionSpeed = motionSpeed;
        this.roll = roll;
    }

    public PlayerInfo() { }

    public void setPos(Vector3 pos)
    {
        x = pos.x; y = pos.y;z = pos.z;
    }

    public void setRot(Vector3 rot)
    {
        ry = rot.y;
    }

}


创建UserClient.cs脚本负责管理与服务端的连接和数据收发,在Start函数中,进行服务端的连接。并且在Update函数中将当前玩家的状态定时传至服务端,并等待服务端响应玩家状态信息列表。注意利用WebSocket定时收发操作均为异步进行。

//客户端

//定义消息类
public class Message
{
    public string type;
    public string info;
    public Message(string _type, string _info)
    {
        type = _type;
        info = _info;
    }
}

public class UserClient : MonoBehaviour
{
    public static float sendMsgCD = 0.1f; //发送数据的时隔
    public static PlayerInfo playerInfo;
    public static Queue<Message> msgQueue = new Queue<Message>();

    static ClientWebSocket webSocket;
    static float ntime = 0;

    private void Awake()
    {
        playerInfo = new PlayerInfo();
        playerInfo.name ="Starry";
    }

    private void Start()
    {
            string ip = "127.0.0.1" ;
        int port = 8888;
        Connect(ip, port);
    }

    private void Update()
    {
        UpdatePlayerInfo();
        HandlePlayerInfo();
    }


    public static void Connect(string ip, int port)
    {
        webSocket = new ClientWebSocket();
        webSocket.ConnectAsync(new Uri("ws://" + ip + ":" + port), CancellationToken.None);
        print("连接成功");
    }

    public static async Task SendMessage(Message msg)
    {
        string str = JsonConvert.SerializeObject(msg);
        byte[] bytes = Encoding.UTF8.GetBytes(str + '&');

        await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);

        // 接收服务器的响应
        byte[] buffer = new byte[1024];
        WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
        string response = Encoding.UTF8.GetString(buffer, 0, result.Count);
        print("服务器响应: " + response);

        msgQueue.Enqueue(JsonConvert.DeserializeObject<Message>(response));

    }

    public static void UpdatePlayerInfo()
    {
         //设置计时实现周期更新数据
        ntime += Time.deltaTime;
        if(ntime > sendMsgCD)
        {
            Task task = SendMessage(new Message("UpdatePlayerInfo", JsonConvert.SerializeObject(playerInfo)));
            ntime = 0;
        }

    }

    public static void HandlePlayerInfo()
    {
        //从玩家状态队列中处理数据
        if (msgQueue.Count > 0)
        {
            Message msg = msgQueue.Dequeue();
            print(msg.type);
            switch (msg.type)
            {
                case "AllPlayerInfo":
                    //获取所有玩家信息的列表
                    Dictionary<string, PlayerInfo> listInfo = JsonConvert.DeserializeObject<Dictionary<string, PlayerInfo>>(msg.info);
                    PlayerPool.ins.updatePlayer(listInfo);//交付PlayerPool处理玩家的状态更新
                    break;
            }
        }
    }
 }


创建PlayerPool.cs脚本用于处理玩家状态的更新(如位置、角度的变换、动画参数等)这里因为要在静态函数里调用该类的方法,注意要创建单例模式。

public class PlayerPool : MonoBehaviour
{
    //联机模块代码
    //为场景内的所有玩家创建模型并同步移动
    public GameObject playerPrefab;
    public bool instantSelf; //联机模式是否生成自己的模型
    Dictionary<string, GameObject> models = new Dictionary<string, GameObject>();  //根据玩家的名字找到对应的模型
    Dictionary<string, PlayerInfo> playerState = new Dictionary<string, PlayerInfo>(); //玩家名字与玩家的状态的字典

    private Vector3 _posVel;

    public void updatePlayer(Dictionary<string, PlayerInfo> list)
    {
        //尝试寻找玩家
        foreach(var p in list)
        {
            if(PlayerPrefs.GetString("PlayerName") != p.Key || instantSelf)
            {
                if (models.ContainsKey(p.Key))
                {
                    playerState[p.Key] = p.Value;
                }
                else
                {
                    print("生成模型");
                    models[p.Key] = Instantiate(playerPrefab, new Vector3(p.Value.x, p.Value.y, p.Value.z),
                        Quaternion.Euler(new Vector3(0, p.Value.ry, 0)));
                    playerState[p.Key] = p.Value;
                }
            }
        }
    }

    private void Update()
    {
        TransmitState();
    }

    private void TransmitState()
    {
        //更新玩家模型的状态
        foreach (var p in playerState)
        {
            Rigidbody _rb = models[p.Key].GetComponent<Rigidbody>();
            Animator _animator = models[p.Key].GetComponent<Animator>();
            _posVel = _rb.velocity;
            models[p.Key].transform.position = Vector3.SmoothDamp(models[p.Key].transform.position,
                new Vector3(p.Value.x, p.Value.y, p.Value.z), ref _posVel, 0.05f);

            models[p.Key].transform.rotation = Quaternion.Euler
               (0, p.Value.ry, 0);

            //更新角色预制体动画状态
            _animator.SetFloat("MotionSpeed", p.Value.motionSpeed);
            _animator.SetFloat("Speed", p.Value.speed);
            _animator.SetBool("Roll", p.Value.roll);
        }
    }

    //采用单例模式
    public static PlayerPool ins;
    private void Awake()
    {
        ins = this;
    }
}


在玩家控制的函数中,可以更新当前玩家的状态信息。这里可以我放在的控制玩家移动的函数Move()中。

private void Move()
{
  //...

  UserClient.playerInfo.setPos(transform.position);
  UserClient.playerInfo.setRot(transform.rotation.eulerAngles);
  UserClient.playerInfo.speed = _animationBlend;
  UserClient.playerInfo.motionSpeed = inputMagnitude;
}


将脚本挂载到场景中:

挂载脚本


大功告成,打开服务端,然后开启多个客户端,就能看到多个玩家在场景中啦。

不过由于目前的玩家状态信息很有限,如跳跃、攻击、血量等还没做,如果后面条件允许也会补上的(画饼)

  • 23
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Unity 是一款强大的游戏开发引擎,但本身并不直接支持 WebSocket 服务端。但是,我们可以借助 Unity网络功能和一些第三方库来实现 WebSocket 服务端。 要实现 WebSocket 服务端,我们可以使用 .NET 库中的 HttpListener 类。首先,我们需要创建一个新的 C# 脚本,并在其中引入 System.Net 命名空间。然后,我们可以创建一个 HttpListener 对象,设置监听的地址和端口。例如: HttpListener httpListener = new HttpListener(); httpListener.Prefixes.Add("http://localhost:8080/"); 接下来,我们需要开始监听这个地址和端口: httpListener.Start(); 然后,我们可以使用异步方式等待客户端的连接: IAsyncResult result = httpListener.BeginGetContext(new AsyncCallback(ListenerCallback), httpListener); 在 ListenerCallback 回调函数中,我们可以获取客户端的连接和请求信息: void ListenerCallback(IAsyncResult result) { HttpListener listener = (HttpListener)result.AsyncState; HttpListenerContext context = listener.EndGetContext(result); // 处理客户端的请求 } 在处理客户端请求的过程中,我们可以根据不同的请求类型,实现相应的 WebSocket 逻辑。使用 .NET 中的 WebSocket 类,我们可以从 HttpListenerContext 中获取 WebSocket 对象,并执行诸如发送消息、接收消息等操作。 需要注意的是,为了实现完整的 WebSocket 逻辑,我们可能还需要处理握手过程中的协议判断、消息编码解码等细节。 综上所述,要在 Unity实现 WebSocket 服务端,我们可以利用 .NET 中的 HttpListener 类来监听客户端连接,并在处理请求过程中实现 WebSocket 的相关逻辑。这样就可以通过 Unity 实现 WebSocket 服务端了。 ### 回答2: Unity 是一款游戏开发引擎,通常用于开发各种类型的游戏。虽然 Unity 自身不支持直接实现 WebSocket 服务端,但我们可以通过使用插件或自定义脚本来实现。 首先,我们可以选择使用第三方插件或库,如 Best HTTP、WebSocket-Sharp 等来在 Unity实现 WebSocket 服务端。这些插件可以通过提供的 API 来创建、监听和处理 WebSocket 连接。我们可以根据项目需求选择最适合的插件,然后按照其文档进行配置和使用。 另外,如果我们希望自己编写 WebSocket 服务端,可以通过使用 Unity 提供的网络相关 API 来实现。首先,我们可以通过 Unity 的 NetworkTransport 类来创建一个基于 UDP 或 TCP 的网络连接。然后,我们可以使用 NetworkTransport.ReceiveFromHost() 方法来接收来自客户端的消息,并使用 NetworkTransport.SendToHost() 方法向客户端发送消息。使用这些 API,我们可以在 Unity实现一个简单的 WebSocket 服务端。 不过需要注意的是,WebSocket 是一种基于 TCP 的双向通信协议,与 HTTP 协议不同。在实现 WebSocket 服务端时,我们需要遵循 WebSocket 的协议规范,并正确处理握手、数据帧等操作。此外,我们还需要考虑并发连接、消息分发等问题,以确保 WebSocket 服务端的稳定性和可靠性。 总结来说,Unity 虽然不直接支持实现 WebSocket 服务端,但我们可以通过使用第三方插件或自定义脚本来实现。无论选择哪种方式,我们都需要理解 WebSocket 的协议规范,并根据需求正确配置和使用相关的插件或 API。 ### 回答3: 使用Unity实现WebSocket服务器可以通过Unity自带的Networking组件以及C#中的WebSocketSharp库来实现。下面是一个简单的步骤: 1. 在Unity中创建一个空的场景。然后创建一个空的游戏对象,并添加一个脚本来处理WebSocket服务器的逻辑。 2. 在脚本中导入WebSocketSharp库。你可以通过下载WebSocketSharp库的源代码,然后将其导入到Unity项目中,或者使用其他方法(如NuGet)从包管理器中引入。 3. 在脚本中添加WebSocket服务器的逻辑。你需要创建一个WebSocket服务器对象,并指定监听的端口号。例如: ``` using WebSocketSharp; using UnityEngine; public class WebSocketServer : MonoBehaviour { private WebSocketServer wsServer; private void Start() { wsServer = new WebSocketServer(12345); // 指定端口号 wsServer.Start(); wsServer.OnMessage += (sender, e) => { Debug.Log("Received message: " + e.Data); // 处理接收到的消息 }; wsServer.OnClose += (sender, e) => { Debug.Log("Connection closed"); // 处理连接关闭的逻辑 }; } private void OnDestroy() { if (wsServer != null) { wsServer.Stop(); wsServer = null; } } } ``` 4. 将脚本挂载到空的游戏对象上。然后按下Play按钮以在Unity中启动WebSocket服务器。 5. 在客户端上使用WebSocket连接到服务器。你可以使用浏览器的WebSocket API或其他WebSocket库来实现。提供服务器的IP地址和端口号,然后进行连接。 这样,你就可以通过Unity实现基本的WebSocket服务器。你可以根据具体的需求在OnMessage和OnClose事件中添加更多逻辑来处理消息和连接的关闭。同时需要注意,Unity的Networking组件也提供了一些网络功能,你也可以尝试使用这些组件来实现WebSocket服务器。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值