线上地址:欢迎各位小伙伴们试玩,并在游戏中提供反馈 ! !
仓库地址: Klotski: 数字华容道+拼图游戏
1.项目介绍:
数字华容道是一款经典的益智游戏,旨在挑战玩家的逻辑思维和空间想象能力。玩家需要通过移动数字方块的位置,按照特定的顺序将数字排列成正确的顺序,从而完成整个拼图。本项目不仅提供了离线单机版还提供双人PK功能,让您和好友在紧张刺激的氛围下一起头脑风暴。
2.项目逻辑:
- 用户点击进入房间后,前端获取访问者的设备号,随机昵称和头像,并存储到本地,用做后续的用户识别。
- 房间内只能存在两个玩家,第一个进行房间的人默认成为房主,向后端发送请求,建立WebSocket通信,后端通过map将用户的唯一Id和WebSocket相绑定。房主点击邀请好友后获得邀请码,分享给好友。
- 好友通过房主分享的邀请码进行房间,生成昵称和头像并向后端发送请求,建立WebSocket通信,同样地后端通过map将好友Id和WebSocket连接进行绑定。数据结构见Client.go
- 此时后端通过roomId将房主和好友进行绑定,方便后期查找,进行各种数据的处理。数据结构见Hub.go
- 好友点击准备,房主点击开始游戏后,双方进入华容道游戏。
- 在游戏过程中,实时交换双方的游戏数据:步数和完成进度。
- 如果一方完成游戏,另一方被告知对方已完成,游戏结束。
- 特殊情况处理:
- 如果在游戏的过程中,一方主动退出游戏,则另一方直接胜利;
- 如果在游戏过程中,玩家出现弱网、断网的情况,则会进行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自动化部署,进行产品迭代