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

线上地址:欢迎各位小伙伴们试玩,并在游戏中提供反馈 ! !

仓库地址: Klotski: 数字华容道+拼图游戏

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通信建立和跨域处理

Go 复制代码
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游戏处理中枢

Go 复制代码
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自动化部署,进行产品迭代
相关推荐
安得权15 小时前
Azure Dataverse 权限设计学习
学习·flask·azure
学烹饪的小胡桃15 小时前
WGCAT工单系统 v1.2.7 更新说明
linux·运维·服务器·网络·工单系统
asdfg125896315 小时前
小程序开发中的JS和Go的对比及用途
开发语言·javascript·golang
云飞云共享云桌面15 小时前
非标自动化工厂的设计云桌面为什么要选云飞云智能共享云桌面?
大数据·运维·服务器·网络·自动化·负载均衡
做cv的小昊16 小时前
【TJU】信息检索与分析课程笔记和练习(6)英文数据库检索—web of science
大数据·数据库·笔记·学习·全文检索
Darkershadow16 小时前
蓝牙学习之uuid与mac
python·学习·ble
2501_9418814016 小时前
在墨西哥城复杂流量环境下构建高稳定性API网关的架构设计与实现实践分享
macos·golang·xcode
毛小茛16 小时前
芋道管理系统学习——项目结构
java·学习
lowhot16 小时前
各种网络协议比较
网络·网络协议
运维有小邓@16 小时前
如何实现基于角色的访问控制?
运维·网络