在游戏开发过程中,有三个核心概念需要明确。
首先是“联盟”这一概念。在游戏开发中,我们通常将其称为“联盟”,但大家也可以将其称为“俱乐部”,在游戏中则体现为“亲友圈”。这三个名称本质上是相同的,只是在不同游戏中可能叫法不同。有的地方叫“联盟”,有的地方叫“俱乐部”,还有的地方叫“亲友圈”,大家需要理解并接受这种概念上的多样性。
在本游戏中,我们采用“亲友圈”的形式,但目前并不打算在游戏内实现亲友圈这部分功能。当我们在游戏开始页面创建房间时,是以普通用户的身份创建的,其他玩家加入房间后一起进行游戏。这与亲友圈的形式有何区别呢?在创建房间时,亲友圈可以通过其特定形式进行创建,也就是说,你可能处于一个类似于联盟的环境中,你在这个联盟里创建房间并进行游戏,你游戏过程中的各种记录数据都会统计到当前联盟中。这与普通玩家单独创建房间的逻辑并无太大差异,唯一的区别在于它被置于联盟之下,这一点需要大家先理解清楚。后续我们在编写代码时,由于是在上层进行操作,需要先串联起联盟,即使是普通用户,也有一个固定的联盟,我们是在这个身份下去进行操作的。
理解了这个概念后,接下来我们会创建一个房间,这个房间就相当于处于我们所说的联盟之下,联盟维护着众多房间。创建房间后,我们会选择第一种玩法,即赢三张,然后进入房间开始游戏。
进入游戏后,房间还涉及另一个概念,即房间是一个很通俗的概念,无论是玩麻将还是赢三张,都需要创建房间。创建完房间后,会有不同的游戏类别,我们将其称为“gametype”。在这个场景下,我们是三张麻将,可以通过不同的游戏事件来渲染房间内不同的场景,包括不同的玩法,形成一种从属关系。
在每一种玩法中,都有其对应的游戏规则,这也是大家需要了解的。游戏规则是在创建房间时选择的,比如人数、玩法,以及高级选项中的不允许搓牌、允许托管、是否允许中途进人、是否允许观战等不同的设置。不同的玩法,如赢三张、麻将等,都有各自不同的游戏规则。
赢三张的基础玩法介绍
在正式开发赢三张游戏之前,我们先来了解一下它的基础玩法,这样在开发过程中就能明白我们正在做的事情。
赢三张的基本规则其实很简单,主要是比大小。牌库由除大小王之外的52张牌组成,每个人随机发三张牌,通过比大小来决定胜负。
三张牌会有不同的组合形式。例如“单牌”,即三张牌各不相同,既不连续,花色也不同。比较单牌时,先从最大的牌开始比,谁的大谁就赢。
“对子”是指两张牌的点数相同,比如两张8。对子的牌型一定大于单牌。如果双方都是对子,则按照对子的规则进行比较。
“顺子”是指花色不同但点数相连的三张牌。而“金花”是指花色相同但点数不相连的三张牌。如果牌型是“顺金”,即花色相同且点数相连,那么这种牌型一定大于金花。而“豹子”则是三张相同的牌,是最大的牌型。
如果双方的牌型相同,比如都是顺子,那么就比较最大的那张牌的点数。比较牌的大小时,从大到小依次进行,花色不分大小,只看数字。如果最大牌相同,则比较第二大的牌,依次类推。如果两副牌的大小完全相同,则主动比牌者获胜。
在游戏过程中,由于大家都是暗牌,不知道对方的牌面大小,这就需要玩家凭借运气和判断来决定是否跟注、加注、看牌或比牌。如果比牌失败,或者主动弃牌,则算作失败。
赢三张游戏的创建房间流程
在开发赢三张游戏时,首先要进入房间,选择赢三张,然后点击创建房间。虽然我们之前提到过亲友圈的概念,但本游戏并不涉及亲友圈的设计。不过,大家需要了解这个概念,因为在创建房间时会涉及相关数据的传递。
点击创建房间后,会发起一个请求,请求创建房间,并传递一些参数,如用户ID、游戏规则(包括局数、最小玩家数、最大玩家数、游戏模式、是否防作弊等)。返回的结果如果没有错误(返回值为0),则表示房间创建成功。
创建成功后,会向客户端推送房间号,客户端收到后会更新用户信息,此时用户已处于房间内。接着,客户端会发起一个房间通知请求,我们则需要推送房间场景信息,包括房间号、创建用户信息、当前游戏规则、房间内用户详细信息(如UID、昵称、头像、钻石、金币、IP地址、性别等)以及座位号等。座位号用于后续的出牌等操作。
推送完这些信息后,客户端会根据这些数据渲染游戏场景,整个房间布置完成后,玩家就可以点击准备,等待其他玩家进入,进而进行下一步操作。
创建房间接口及相关逻辑开发
我们现在要做的事情是开发一个创建房间的接口,接收相关信息后,创建房间并生成房间号,然后将房间号和游戏类型推送给客户端,告知其可以进入房间。进入房间后,前端会发送场景信息通知,我们再推送当前场景的详细信息,完成游戏场景的加载,这样创建房间的第一步操作就完成了。
接下来,我们需要创建对应的游戏服务,并编写处理创建房间请求的逻辑。根据API的要求,我们需要创建game服务和hall,注册Handler,并在其中处理相关请求。
在创建房间的逻辑中,我们需要先根据参数创建房间,然后生成房间号,并将房间号、游戏类型等信息推送给客户端。同时,我们还需要判断用户是否已经在房间中,避免重复创建房间。
创建房间的具体步骤包括:生成房间号,将房间号推送给客户端,将游戏类型推送给客户端,以及推送房间场景信息。在生成房间号时,我们需要确保其唯一性,可以通过随机数等方式生成,并在联盟中进行判断,避免重复。
在用户进入房间时,我们需要将用户信息转化为对应俱乐部的信息,更新数据库,并通知其他玩家该用户进入房间。同时,向该用户推送其进入游戏的消息,触发进入对应游戏。
package room
import (
"common/biz"
"context"
"core/models/entity"
"core/services"
"framework/msError"
"framework/remote"
"game/component/proto"
"game/component/sz"
"sync"
"time"
)
type Room struct {
sync.RWMutex
userInfoService *services.UserInfoService
Id string
CreatorInfo *proto.RoomUserInfo
GameRule proto.GameRule
UserArr map[string]*proto.UserRoom
GameFrame GameFrame
gameStarted bool
roomDismissed bool //解散
CreateTime int64
ActiveTime int64
}
func (r *Room) GetUserArr() map[string]*proto.UserRoom {
r.RLock()
defer r.RUnlock()
return r.UserArr
}
func NewRoom(service *services.UserInfoService, roomId string, userData *proto.RoomUserInfo, gameRule proto.GameRule) *Room {
r := &Room{
Id: roomId,
CreatorInfo: userData,
GameRule: gameRule,
UserArr: make(map[string]*proto.UserRoom),
CreateTime: time.Now().UnixMilli(),
userInfoService: service,
}
//b := base.NewCommon(r)
if proto.GameType(gameRule.GameType) == proto.PinSanZhang {
r.GameFrame = sz.NewGameFrame(r.GameRule, r)
}
return r
}
func (r *Room) Close() {
r.GameFrame.Close()
}
func (r *Room) UserEntryRoom(session *remote.Session, userInfo *entity.User) *msError.Error {
if r.roomDismissed {
return biz.NotInRoom
}
var unionUserInfo proto.UnionUserInfo
//将用户信息转化为对应俱乐部的信息
if r.CreatorInfo.CreatorType == proto.UnionCreate {
unionUserInfo = proto.BuildUnionUserInfo(userInfo, r.CreatorInfo.UnionID)
} else {
unionUserInfo = proto.BuildUnionUserInfo(userInfo, proto.NormalRoom)
}
r.Lock()
defer r.Unlock()
user, ok := r.UserArr[userInfo.Uid]
if !ok {
chairID := len(r.UserArr)
user = &proto.UserRoom{
UserStatus: proto.None,
UserInfo: &unionUserInfo,
ChairID: chairID,
}
r.UserArr[userInfo.Uid] = user
//更新数据库
r.userInfoService.UpdateUserRoomByUid(context.TODO(), userInfo.Uid, r.Id)
//通知更新用户信息
user.UserInfo.RoomID = r.Id
r.UpdateUserInfoRoomPush(user, session)
}
session.Put("roomId", r.Id)
//向其他玩家推送自己进入房间的消息
r.otherEntryRoomPush(user, session)
//推送玩家进入游戏消息 触发进入对应游戏
r.SelfEntryRoomPush(user, session)
return nil
}
func (r *Room) UpdateUserInfoRoomPush(user *proto.UserRoom, session *remote.Session) {
pushMsg := map[string]string{
"roomID": user.UserInfo.RoomID,
"pushRouter": "UpdateUserInfoRoomPush",
}
session.Push([]string{user.UserInfo.Uid}, pushMsg, "ServerMessagePush")
}
func (s *Session) Put(key string, value any) {
s.Lock()
s.Unlock()
s.data[key] = value
//直接推送到ws的session中 进行保存
s.pushSessionChan <- s.data
}
func (r *Room) otherEntryRoomPush(user *proto.UserRoom, session *remote.Session) {
pushMsg := map[string]any{
"type": proto.OtherUserEntryRoomPush,
"data": map[string]any{
"roomUserInfo": user,
},
"pushRouter": "RoomMessagePush",
}
users := make([]string, 0)
for _, v := range r.UserArr {
if v.UserInfo.Uid == user.UserInfo.Uid {
continue
}
users = append(users, v.UserInfo.Uid)
}
session.Push(users, pushMsg, "ServerMessagePush")
}
func (r *Room) SelfEntryRoomPush(user *proto.UserRoom, session *remote.Session) {
pushMsg := map[string]any{
"gameType": r.GameRule.GameType,
"pushRouter": "SelfEntryRoomPush",
}
session.Push([]string{user.UserInfo.Uid}, pushMsg, "ServerMessagePush")
}