目录
文章目录
一、进入游戏服务
//- 1创建game server
//- 2在controller层完成路由初始化和业务逻辑
// - 2-1加载基础、地图资源、地图单元格、城池设施、武将、技能配置到gameConfig包的结构体里
// - 2-2添加角色路由和做进入的游戏的逻辑
// - 2-2-1查询用户及角色资源\初始化玩家属性\城池
// - 2-2-2查询角色拥有的属性(资源、城池、建筑、部队、武将)
// - 2-3添加地图路由和做地图相关的逻辑
// - 2-4做初始化城池逻辑:根据城市id,从城市-设施表查找对应项,然后将配置文件中的城市设施填充到"城市-设施"结构体,再写回数据库
// - 2-5做标记逻辑:存储role_attribute 的pos_tag 数据,[{“x”:74,“y”:81,“name”:“土地Lv.2”}]
//- 3启动服务
1、
func main() {
host := config.File.MustValue("game_server", "host", "127.0.0.1")
port := config.File.MustValue("game_server", "port", "8001")
s := net.NewServer(host + ":" + port) //- 1创建game server
s.NeedSecret(false)
game.Init() //- 2在controller层完成路由初始化和业务逻辑
s.Router(game.Router) //将初始化的路由赋给server
s.Start() //- 3启动服务
}
func Init() {
db.TestDB()
// - 2-1加载基础和地图资源配置
//加载基础配置
gameConfig.Base.Load()
//加载地图的资源配置
gameConfig.MapBuildConf.Load()
initRouter()
}
// - 2-2添加角色路由和做进入的游戏的逻辑
func (r *RoleController) Router(router *net.Router) {
g := router.Group("role")
g.AddRouter("enterServer", r.enterServer)
g.AddRouter("myProperty", r.myProperty)
}
// - 2-2-1查询用户及角色资源\初始化玩家属性\城池
func (r *RoleController) enterServer(req *net.WsMsgReq, rsp *net.WsMsgRsp) {
//Session 需要验证是否合法 合法的情况下 可以取出登录的用户id
//根据用户id 去查询对应的游戏角色,如果有 就继续 没有 提示无角色
//根据角色id 查询角色拥有的资源 roleRes,如果资源有 返回,没有 初始化资源
reqObj := &model.EnterServerReq{}
rspObj := &model.EnterServerRsp{}
err := mapstructure.Decode(req.Body.Msg, reqObj)
rsp.Body.Seq = req.Body.Seq
rsp.Body.Name = req.Body.Name
if err != nil {
rsp.Body.Code = constant.InvalidParam
return
}
session := reqObj.Session //校验session合法性即解析token
_, claim, err := utils.ParseToken(session)
if err != nil {
rsp.Body.Code = constant.SessionInvalid
return
}
uid := claim.Uid
err = logic.RoleService.EnterServer(uid, rspObj, req.Conn)
if err != nil {
rsp.Body.Code = err.(*common.MyError).Code()
return
}
rsp.Body.Code = constant.OK
rsp.Body.Msg = rspObj
}
// - 2-2-2查询角色拥有的属性(资源、城池、建筑、部队、武将)
func (r *RoleController) myProperty(req *net.WsMsgReq, rsp *net.WsMsgRsp) {
//分别根据角色id 去查询 军队 资源 建筑 城池 武将
role, err := req.Conn.GetProperty("role")
if err != nil {
rsp.Body.Code = constant.SessionInvalid
return
}
rsp.Body.Seq = req.Body.Seq
rsp.Body.Name = req.Body.Name
rid := role.(*data.Role).RId
rspObj := &model.MyRolePropertyRsp{}
//资源
rspObj.RoleRes, err = logic.RoleService.GetRoleRes(rid)
if err != nil {
rsp.Body.Code = err.(*common.MyError).Code()
return
}
//城池
rspObj.Citys, err = logic.RoleCityService.GetRoleCitys(rid)
if err != nil {
rsp.Body.Code = err.(*common.MyError).Code()
return
}
//建筑
rspObj.MRBuilds, err = logic.RoleBuildService.GetBuilds(rid)
if err != nil {
rsp.Body.Code = err.(*common.MyError).Code()
return
}
//军队
rspObj.Armys, err = logic.ArmyService.GetArmys(rid)
if err != nil {
rsp.Body.Code = err.(*common.MyError).Code()
return
}
//武将
rspObj.Generals, err = logic.GeneralService.GetGenerals(rid)
if err != nil {
rsp.Body.Code = err.(*common.MyError).Code()
return
}
rsp.Body.Code = constant.OK
rsp.Body.Msg = rspObj
}
2-5 做标记逻辑
数据库就是存储role_attribute 的pos_tag 数据,[{“x”:74,“y”:81,“name”:“土地Lv.2”}]
具体实现:controller层先从conn中获取角色,然后service层根据角色id查询角色属性(roleAttribute)
2-6 我的武将
路由:general.myGenerals
首先加载武将信息,然偶后controller层根据角色id查询武将,如果没有武将,则随机生成三个武将
2-7 我的军队
路由:army.myList
军队不用初始化,刚进来没有军队,军队跟城市绑定,根据城市id查询军队
2-8 我的战报
涉及war_report
表
controller处理战报请求,并从conn连接中取出角色,service层根据角色id查询角色所有战报,涵盖攻防所有战报,最后将data层查询出的数据转成客户端需要的数据返回即可.
2-9 我的技能
设计skill.list
表,包括技能id,归属武将等
跟我的战报差不多,就把战报换成技能接口
二、一些优化和功能
1、事务
用于保证多个业务逻辑过程同时成功,只要有一个出错就不会写入数据库.
session := db.Engine.NewSession()//新建事务
defer session.close()
session.Begin()//开启事务
//将db.Engine替换成session由session接管增删改查操作.如:
_,err := session.Table(roleRes).Insert(roleRes)
//如果有逻辑出错使用Session.Rollback()然后return,其实不Rollback也可以,因为出错后return了,没有执行session.Commit(),就不会固化到数据库
session.Commit()
为了保证事务在不同服务相同链路间传递,可以在请求结构体中添加一个上下文context,在service层将session放到ctx中,就可以使用事务控制不同服务业务逻辑一致性
2、中间件
go语言中,中间件的应用非常广,“中间件”通常意思是“包装原始应用并添加一些额外的功能的应用的一部分”。比如日志,权限,认证等
比如:在我们的应用中,很多接口都要用到角色的信息,我们需要从conn中进行获取,并进行判断其是否存在,我们可以将这个判断用于是否拥有角色的业务逻辑抽取出去,进行统一验证。
实现过程:
- 1.定义中间件结构体,在路由组结构体里添加中间件属性
- 2.定义新增"路由和组中间件"的函数
- 3.定义具体中间件,并使用2的函数,将中间件添加到结构体保存
- 4.在执行路由请求对应函数前,执行中间件逻辑
// - 1.定义中间件结构体
type HandlerFunc func(req *WsMsgReq, rsp *WsMsgRsp)
type MiddlewareFunc func(handlerFunc HandlerFunc) HandlerFunc
// - 2.在路由组结构体里添加中间件
type group struct {
mutex sync.RWMutex
prefix string
handlerMap map[string]HandlerFunc
middlewareMap map[string][]MiddlewareFunc //某个路由需要执行的对应的中间件
middlewares []MiddlewareFunc //整个组里需要执行的中间件
}
// - 3.定义添加路由中间件
func (g *group) AddRouter(name string, handlerFunc HandlerFunc, middlewares ...MiddlewareFunc) {
g.mutex.Lock()
defer g.mutex.Unlock()
g.handlerMap[name] = handlerFunc
g.middlewareMap[name] = middlewares
}
// - 4.定义添加组中间件
func (g *group) Use(middlewares ...MiddlewareFunc) {
g.middlewares = append(g.middlewares, middlewares...)
}
// - 5.定义添加检查角色中间件
func CheckRole() net.MiddlewareFunc {
return func(next net.HandlerFunc) net.HandlerFunc {
return func(req *net.WsMsgReq, rsp *net.WsMsgRsp) {
log.Println("进入到角色检测....")
_ , err := req.Conn.GetProperty("role")
if err != nil {
rsp.Body.Code = constant.SessionInvalid
return
}
next(req,rsp)//检查完毕执行请求函数,中间件定义的时候,定义为请求函数类型,执行完中间件逻辑后,继续执行请求函数即可
}
}
}
// - 6.定义添加日志打印中间件
func Log() net.MiddlewareFunc {
return func(next net.HandlerFunc) net.HandlerFunc {
return func(req *net.WsMsgReq, rsp *net.WsMsgRsp) {
log.Println("请求路由",req.Body.Name)
log.Println("请求参数",fmt.Sprintf("%v",req.Body.Msg))
next(req,rsp)
}
}
}
// - 7.添加组和路由中间件
func (r *RoleController) Router(router *net.Router) {
g := router.Group("role")
g.Use(middleware.Log())
g.AddRouter("create", r.create)
g.AddRouter("enterServer", r.enterServer)
g.AddRouter("myProperty", r.myProperty, middleware.CheckRole())
g.AddRouter("posTagList", r.posTagList, middleware.CheckRole())
}
// - 8.执行中间件
func (g *group) exec(name string, req *WsMsgReq, rsp *WsMsgRsp) {
h, ok := g.handlerMap[name] //如果name有处理函数,则执行中间件和请求函数
if !ok {
h, ok = g.handlerMap["*"] //复用ok,如果有*,即过滤任何路由,则也执行中请求函数
if !ok {
log.Println("路由未定义")
}
}
if ok {
//- 8.在执行路由之前,需要执行中间件代码
for i := 0; i < len(g.middlewares); i++ {
h = g.middlewares[i](h) //使用i中间件处理请求函数h
}
mm, ok := g.middlewareMap[name]
if ok {
for i := 0; i < len(mm); i++ {
h = mm[i](h)
}
}
h(req, rsp) //继续处理请求函数
}
}
3、扫描地图资源请求
鼠标移动时,需要扫描对应位置地图资源
路由: nationMap.scanBlock
业务逻辑实现流程:在controller层分别调用service层的"扫描角色建筑"、“扫描角色城池”、"扫描角色军队"三个服务,获取结果填充到rsp返回即可.
以查询角色建筑为例:首先,在初始化时从配置文件里load系统和玩家建筑,保存到service层的角色建筑结构体里。然后controller层调用角色建筑sevice层ScanBlock方法,将x,y传入获取该点角色建筑.
角色建筑因为存在占领,需要写数据库,而角色城池,只需要查询显示不同玩家城池.
“扫描玩家军队”:军队信息存储在一个map中,map的key是坐标点,value还是一个map,此map的key是armyID。该功能会遍历查找某块地图半径范围内所有军队(传入的是点和length,和角色id,据此进行查询),然后判断军队是否在角色视野范围内或者是盟友,如果是就加入结果,最后返回结果即可.
4、创建角色
路由:role.create
参数:
type CreateRoleReq struct {
UId int `json:"uid"`
NickName string `json:"nickName"`
Sex int8 `json:"sex"`
SId int `json:"sid"`
HeadId int16 `json:"headId"`
}
根据前端传递请求往数据库插入角色,并返回角色信息即可
三、
1. 查询下次征收时间
路由:interior.openCollect
根据角色id查询role attribute表,根据collect_times征收次数和last_collect_time最后征收时间进行计算
过程:在role attribute service层先将信息查询缓存到本地map.然后从map中获取rid,根据rid获取征收次数和征收间隔,如果今天征收次数用完,则间隔24小时再次征收,如果次数没用完,下次征收就是最后征收时间+间隔
2.征收资源
路由:interior.collect
1.查询角色属性 获取征收的相关信息(征收次数,最后一次征收时间)
2.查询角色资源 得到当前的金币(Gold)
3.查询获取当前的产量(yield) 和 征收的金币是多少
4.将当前金币和征收金币相加获取最新金币,向dao层的channel发送一个更新消息,角色资源dao层在init方法开启一个go协程监听channel消息,一旦监听到消息,就更新角色资源表.
5.然后计算产量,也向角色属性dao层发送一个消息,也用channel固化数据库.
6.最后将金币和征收结果返回给前端即可
3.怎么控制资源刷新?或怎么定期获取资源?
在角色资源里load加载资源的时候,起一个go协程每隔一段时间获取资源和容量,在未超容量前提下,不断增加资源即可
武将回复体力类似
在武将load加载资源完毕,起一个go协程每隔一段时间获取体力和体力上线,在未超容量前提下,不断增加体力。
4. 征兵
路由:army.conscript
//征兵
type ConscriptReq struct {
ArmyId int json:"armyId"
//队伍id
Cnts []int json:"cnts"
//征兵人数 [20,20,0]
}
type ConscriptRsp struct {
Army Army json:"army"
RoleRes RoleRes json:"role_res"
}
请求参数是军队id和各武将需征兵人数的数组,返回军队和征兵完剩余资源
征兵实现过程:首先查询角色和军队,然后根据武将和募兵所等级计算征兵上限,确保武将请求的征兵数满足上限。最后计算征兵消耗资源,更新每个武将对应的士兵人数(每个城池最多3个武将,同时军队也对应3个征兵槽)。
4.1 查看某一个部队详情
路由:army.myOne
请求参数“城市id”和“部队id”,返回“Army军队”
实现过程:根据“城市id”和“部队id”查询“Army军队”返回即可
派遣军队
路由:army.assign
请求参数"军队id"、“命令(如攻击、驻军)”、“移动位置坐标”,返回"Army详情"
-
1初始化时开启两个协程监听军队到达和到达后处理
check协程:不断检测军队是否到达,一直遍历一个map,key是到达时间,value是到达军队,若当前时间>到达时间,就将到达军队作为信号发给running协程处理
running协程:监听到达军队channel,一旦有军队到达,就开启一场战争 -
1.1城池攻打处理
查询城池是否有驻守军队(驻守军队也是map,key是positionId,value是军队)
如果没有军队驻守,则直接攻打城池耐久
如果有军队,就处理战斗,对于每个敌军都会产生战斗、生成战报、升级将领,如果战斗成功,则从map里删除驻军,否则就更新战报就可
具体战斗过程为(随机出手,根据攻击和防御、兵种的克制扣减士兵;结束的条件 主将 士兵为0 或者到达最大回合数) -
1.2 建筑攻打处理
与上面不同的是:没有玩家军队驻守的情况下,随机生成NPC军队进行战斗,其他相同。 -
2查询军队,执行命令
-
2.1占领
首先确保军队能出战、土地非山地和自己或盟友城池、体力够用
然后扣减体力,将到达时间和到达军队加入map
3.联盟
3.1 联盟列表
路由:union.list
实现逻辑:在init方法中load角色属性到map中保存
(key是角色id,值是角色属性,包括联盟id),并将联盟id设置进联盟的每个成员list方法就是遍历这个联盟map
type Union struct {
Id int json:"id"
//联盟id
Name string json:"name"
//联盟名字
Cnt int json:"cnt"
//联盟人数
Notice string json:"notice"
//公告
Major []Major json:"major"
//联盟主要人物,盟主副盟主
}
涉及到的表:
CREATE TABLE `coalition` (
`id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '联盟名字',
`members` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '成员',
`create_id` int(0) UNSIGNED NOT NULL COMMENT '创建者id',
`chairman` int(0) UNSIGNED NOT NULL COMMENT '盟主',
`vice_chairman` int(0) UNSIGNED NOT NULL COMMENT '副盟主',
`notice` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '公告',
`state` tinyint(0) UNSIGNED NOT NULL DEFAULT 1 COMMENT '0解散,1运行中',
`ctime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '联盟' ROW_FORMAT = Dynamic;
CREATE TABLE `coalition_apply` (
`id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id',
`union_id` int(0) UNSIGNED NOT NULL COMMENT '联盟id',
`rid` int(0) UNSIGNED NOT NULL COMMENT '申请者的rid',
`state` tinyint(0) UNSIGNED NOT NULL DEFAULT 0 COMMENT '申请状态,0未处理,1拒绝,2通过',
`ctime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '联盟申请表' ROW_FORMAT = Dynamic;
CREATE TABLE `coalition_log` (
`id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id',
`union_id` int(0) UNSIGNED NOT NULL COMMENT '联盟id',
`op_rid` int(0) UNSIGNED NOT NULL COMMENT '操作者id',
`target_id` int(0) UNSIGNED NULL DEFAULT NULL COMMENT '被操作的对象',
`des` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '描述',
`state` tinyint(0) UNSIGNED NOT NULL COMMENT '0:创建,1:解散,2:加入,3:退出,4:踢出,5:任命,6:禅让,7:修改公告',
`ctime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '发生时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '联盟日志表' ROW_FORMAT = Dynamic;
3.2 联盟详情
路由:union.info
从前端接收联盟id,返回联盟详情即可
3.3 申请列表
路由:union.applyList
根据联盟id,查找联盟,然后比较当前角色是否是联盟主席,如果是才返回申请人相关信息,不是则返回空
4.武将
4.1 武将抽卡
路由:general.drawGeneral
参数:抽卡次数
返回值:武将切片
实现逻辑:
- 计算抽卡花费的金钱
- 判断金钱是否足够
- 判断角色卡池是否足够 (待抽卡的次数 + 已有的武将<限制 )
- 根据次数随机生成武将
- 扣除金币 返回即可
4.2 给城市配置武将
路由:army.dispose
配置武将就是将某个武将配置到某个城市的哪一队的哪一个位置(如:将"张良"配置到"长城"的"第1队"的"位置1")
确保主城存在且属于该角色
确保将领存在且属于该角色
确保"第几大队"小于主城校场等级(3级校场允许有1\2\3队)
查询当前主城的军队 没有就创建,然后确保该军队在主城内,即军队坐标和主城坐标重合,然后进行上下阵操作
如果请求位置为-1则是下阵请求:确保武将空闲或不在征兵状态,然后把将领状态设为"未上阵"状态,即"将军队的将领状态设为"下阵",将领的主城id设为0(没有主城)
否则就是上阵请求:要确保将领空闲或不在征兵状态,同时确保新军队和将领之前状态为"未上阵",然后确保所有将领cost之和满足条件(cost小于一定值,该值随主城等级增加而增加,用于控制将领个数),最后用新军队将领代替旧军队将领,并设置新将领所属城市即可
5. 城池设施
1.查询
路由:city.facilities
根据城市id返回城池设施列表:先提前从数据库中查询所有城市设施,然后存储到service层的map中,key是城市id,value是城市设施的切片.
2.升级设施
路由:city.upFacility
- 需要根据城池id 查询城池
- 根据城池id和角色id查询城池的设施
- 升级设施,需要当前剩余升级时间upTime==0, 而且资源否符合
- 升级完成 设施更新和资源减少的内容固话到数据库
- 资源查询出来 返回前端
6.交易
路由:interior.transform
请求结构体:
type TransformReq struct {
From []int json:"from"
//0 Wood 1 Iron 2 Stone 3 Grain
To []int json:"to"
//0 Wood 1 Iron 2 Stone 3 Grain
}
我们要做的是把From数组内容转到To数组
首先,做交易的时候在主城做交易,需要根据角色id查找城市id,再根据城市id查找"集市"level的城市设施存在
然后,根据角色id查找角色拥有的资源,再把角色资源减去"From数组资源"、“加上To数组资源”,最后将资源通过channel写回数据库即可
四 、
1、各层数据结构你是怎么放的?
数据库对应的结构体 放到 data目录
客户端需要的数据 放到 model目录