问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

基于WebSocket通信的H5小游戏开发总结

创作时间:
作者:
@小白创作中心

基于WebSocket通信的H5小游戏开发总结

引用
CSDN
1.
https://blog.csdn.net/wangye135/article/details/136646040

本文将介绍一个基于WebSocket通信的H5小游戏项目,这是一个数字华容道游戏,支持单人和双人对战模式。文章将详细介绍项目的功能逻辑、代码实现以及开发经验总结。

1.项目介绍

数字华容道是一款经典的益智游戏,旨在挑战玩家的逻辑思维和空间想象能力。玩家需要通过移动数字方块的位置,按照特定的顺序将数字排列成正确的顺序,从而完成整个拼图。本项目不仅提供了离线单机版还提供双人PK功能,让您和好友在紧张刺激的氛围下一起头脑风暴。

2.项目逻辑

  1. 用户点击进入房间后,前端获取访问者的设备号,随机昵称和头像,并存储到本地,用做后续的用户识别。
  2. 房间内只能存在两个玩家,第一个进行房间的人默认成为房主,向后端发送请求,建立WebSocket通信,后端通过map将用户的唯一Id和WebSocket相绑定。房主点击邀请好友后获得邀请码,分享给好友。
  3. 好友通过房主分享的邀请码进行房间,生成昵称和头像并向后端发送请求,建立WebSocket通信,同样地后端通过map将好友Id和WebSocket连接进行绑定。数据结构见Client.go
  4. 此时后端通过roomId将房主和好友进行绑定,方便后期查找,进行各种数据的处理。数据结构见Hub.go
  5. 好友点击准备,房主点击开始游戏后,双方进入华容道游戏。
  6. 在游戏过程中,实时交换双方的游戏数据:步数和完成进度。
  7. 如果一方完成游戏,另一方被告知对方已完成,游戏结束。
  8. 特殊情况处理:
    1. 如果在游戏的过程中,一方主动退出游戏,则另一方直接胜利;
    2. 如果在游戏过程中,玩家出现弱网、断网的情况,则会进行3次短线重连的提示,超过三次后失败后,则直接算作退出房间,留在房间内的玩家胜利。

游戏设计示意图

3.代码实现

本部分只说明项目中比较重要的代码实现,其余代码实现请查看git仓库

3.1项目结构

项目接口分为两部分,http请求(httpLink包)和websocket通信(socket包)。

在router包指定路径和特定的请求处理器。

在pojo包中定义了常用的数据接口和websocket信息交互格式。

3.2WebSocket通信建立和跨域处理

func WsHandle(hub *pojo.HupCenter) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        //将短连接 升级成 长连接-建立WebSocket通信
        upgrade := websocket.Upgrader{
            ReadBufferSize:  1024,
            WriteBufferSize: 1024,
            CheckOrigin: func(r *http.Request) bool {
                return true
            },
        }
        conn, err := upgrade.Upgrade(w, r, nil)
        if err != nil {
            log.Println("conn错误", err)
            return
        }
        fmt.Println("远程主机连接成功 IP为", conn.RemoteAddr())
        //进行client的初始化操作
        client := &pojo.Client{User: &pojo.User{}, Hub: &pojo.HupCenter{}} //非字段不要为nil
        client.Hub = hub
        client.User.UserConn = conn
        client.User.HealthCheck = time.Now().Add(time.Duration(pkg.HeartCheckSecond) * time.Second) //健康时间
        //计时器 : 如果用户规定秒内没有完成用户认证,则直接断开连接
        time.AfterFunc(time.Duration(pkg.UserAuthSecond)*time.Second, func() {
            if !client.User.UserCer {
                fmt.Println("用户认证失败,关闭连接")
                client.User.Close()
            }
        })
        //接受信息 根据信息类型进行分别处理
        go Controller(client)
    }
}

3.3WebSocket游戏处理中枢

func Controller(client *pojo.Client) {
    defer func() {
        client.Hub.UnRegister <- client
    }()
    for {
        _, p, err := client.User.UserConn.ReadMessage()
        if err != nil {
            if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
                //用户主动给关闭连接后的输出
                log.Println("WebSocket closed")
            } else {
                //服务器主动断开走这个,从一个断开的连接中读取信息
                log.Println("server.go conn.ReadMessage 读取信息错误", err)
            }
            return
        }
        var requestPkg pojo.RequestPkg
        err = json.Unmarshal(p, &requestPkg)
        if err != nil {
            fmt.Println("websocket反序列化失败", err)
            return
        }
        //2. 在信息中枢处根据消息类型进行特定的处理
        switch requestPkg.Type {
        case pojo.CertificationType:
            //用户认证
            client.CertificationProcess(requestPkg)
        case pojo.CreateRoomType:
            //创建房间号,并将创建者加入房间
            fmt.Println("发起创建房间的请求")
            client.CreateRoomProcess()
        case pojo.JoinRoomType:
            //1.加入房间的前提,先建立连接
            //2.完成用户认证
            //3.发送消息类型和房间号 Type uuid
            //只有完成上述步骤,才可以加入房间
            var data map[string]interface{}
            err = json.Unmarshal([]byte(requestPkg.Data), &data)
            if err != nil {
                fmt.Println("解析 JSON 失败:", err)
                return
            }
            uuidValue, ok := data["uuid"].(string)
            if !ok {
                fmt.Println("uuid 字段不存在或不是字符串类型")
                return
            }
            client.JoinRoomProcess(uuidValue)
        case pojo.RefreshScoreType:
            //什么是否进行分数更新,前端判断 type:RefreshScoreType, data:step、step、score
            //当用户的行为触发前端游戏机制的更新时,前端调用此接口,后端进行分数的转发 不需要做业务处理,直接转发即可
            fmt.Println("游戏交换中数据", client)
            client.RefreshScoreProcess(requestPkg)
        case pojo.DiscontinueQuitType:
            client.DiscontinueQuitProcess()
        case pojo.GameOverType:
            //游戏结束类型好像没有太大用,游戏结束的时候的提醒,通过分数更新就可以实现了
            fmt.Println("GameOverType")
        case pojo.HeartCheckType:
            //开启一个协程遍历hub中的Client,进行健康检测,生命时间是否会过期,如果过期进行逻辑删除和关闭连接
            if requestPkg.Data == "PING" {
                client.HeartCheckProcess()
            }
        }
    }
}

3.4维护建立连接的客户端

维护已经建立的客户端连接和进行客户端的之间的配对、查询。这里使用管道对全局唯一的map进行操作,防止出现多个协程操作同一个map。

package pojo
import (
    "fmt"
    "klotski/pkg"
    "time"
)
type HupCenter struct {
    ClientsMap map[string]map[string]*Client `json:"-"` //第一个string-roomId 第二个string-userId
    Register   chan *Client
    UnRegister chan *Client
}
// NewHub 初始化一个hub
func NewHub() *HupCenter {
    return &HupCenter{
        ClientsMap: make(map[string]map[string]*Client),
        Register:   make(chan *Client, 1),
        UnRegister: make(chan *Client, 1),
    }
}
// Run 用户向hub中的逻辑注册、删除、心跳检测全方法
func (h *HupCenter) Run() {
    checkTicker := time.NewTicker(time.Duration(pkg.HeartCheckSecond) * time.Second)
    defer checkTicker.Stop()
    for {
        select {
        case client := <-h.Register:
            //先查询是否存在此一个roomId key
            if myMap, ok := client.Hub.ClientsMap[client.User.RoomId]; ok { //有,加入房间
                //检测人数
                if len(myMap) == 1 {
                    myMap[client.User.UserId] = client
                }
            } else { //没有,创建房间
                myMap := make(map[string]*Client)
                myMap[client.User.UserId] = client                //userId
                client.Hub.ClientsMap[client.User.RoomId] = myMap //roomId
            }
            fmt.Println("有人加入房间:", client.Hub.ClientsMap)
        case client := <-h.UnRegister:
            client.User.Close()
            if value, ok1 := client.Hub.ClientsMap[client.User.RoomId]; ok1 {
                if _, ok2 := value[client.User.UserId]; ok2 {
                    delete(value, client.User.UserId)
                }
            }
            if len(client.Hub.ClientsMap[client.User.RoomId]) == 0 {
                delete(client.Hub.ClientsMap, client.User.RoomId)
            }
        case <-checkTicker.C:
            for _, roomMap := range h.ClientsMap {
                //fmt.Println(roomMap)
                for _, client := range roomMap {
                    //fmt.Println(client)
                    //fmt.Println(client.User.HealthCheck)
                    if client.User.HealthCheck.Before(time.Now()) {
                        h.UnRegister <- client
                    }
                }
            }
            fmt.Println(time.Now().Format(time.DateTime), h.ClientsMap)
        }
    }
}
// QueryOtherUser 读操作 -- 根据当前用户寻找另一位用户,返回user对象
func (h *HupCenter) QueryOtherUser(c *Client) *Client {
    if roomMap, ok := h.ClientsMap[c.User.RoomId]; ok { //room
        for userId, user := range roomMap {
            if userId != c.User.UserId {
                return user
            }
        }
    }
    return nil
}

4.项目收获

  • 前期的设计要明确具体,提前构思好项目的整体交互、处理流程,不要把一切问题推迟到编码阶段解决,要学会使用工具,将自己的项目构思表达出来。
  • WebSocket通信的应用场景。
  • 如何设计WebSocket数据包和客户端、服务端的通信流程
  • 将管道和协程熟练运用到自己的项目中
  • 掌握go语言打包部署流程,使用Jenkin自动化部署,进行产品迭代
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号