1 Star 0 Fork 5

BlackHawk / gogame

forked from 暖阳 / gogame 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

gogame

介绍

go实现的游戏逻辑框架

  • 目录文件
  • 通讯流程
  • 功能开发流程

架构图

gogame_png_1

目录文件

cmd: 命令行

game 游戏逻辑模块

    manage
        configmanange 配置管理 【g.GC】 采用LuBan生成json和sturt 在此基础上2次封装
        modulemanange 功能模块管理 【g.M】
        modelmanange 数据层管理 【g.DATA】
        
    module(存放所有的功能模块)
        user:用户模块
            api 接口基类
            api_login 用户登录
            api_loginout 用户登出
                
            fun:模块公共逻辑方法
        hero:同上
        route:所有模块接口注册
        
    common:游戏逻辑常用方法封装
    g:游戏全局使用封装
    globalplayer:玩家全局数据
    
gameconfig:游戏配置相关
  crossserver:跨服配置处理
  serverconfig:服务器配置处理
  
gameservice:游戏服务
    gateway(网关服务,维护客户端链接)
        app:网关服务载体
        handle:用户网关实例
        rpcmanage:管理rpcXClient
        sender: 玩家数据推送
    worker(游戏逻辑服务)
        app:游戏逻辑服务载体
        handle: 接口分发,注射
    service:基类service
    servicemanage:服务管理

log:存在游戏日志,以小时切割文件
    worker:
        2022-7-14-15.log
    gateway:
        2022-7-14-15.log
        
json:自定义json
    crosstimer.json  跨服定时器配置
    timer.json  本服定时器配置
    errormsg.json 异常拦截语言包
    
samejson:策划json
    hero.json  英雄表
    item.json  道具表
    
logger:日志模块

server:网络服务
    go_rpc: 自实现rpc
    rpcx:基于rpcx的2次封装
      client
          client
          selector 自定义worker选择器
      server 服务端
          server
          serverplugin 中间件
    network:基础服务
        tcp:tcp
        ws:websocket
        
lib: 第三方库封装,自定义工具
  database:数据库相关
  convert:类型转换
  emitter:event事件管理
  json:json编解码
  file:文件操作
  protobuf:protobuf操作
  random:随机相关处理
  time:时间相关处理
  sys:通用方法
  
pb: protobuf存储
    pb_python pb.python存储
    proto 定义所有protobuf文件
        模块名
            模块名_api.proto 所有接口的proto
            模块名_mongo.proto  mongodb表对应的proto 
    pb.go都存储在当前目录
        
test:测试模块
    debug:各种调试都可以
    python python相关逻辑
        yace 压测脚本

config: 服务配置文件
main: 服务启动入口
pb2go.py: protobuf转go和python
json2go.py: json转go
 

通讯流程

通讯方式: protobuf

protobuf转换说明

1:客户端发送pb消息到gateway
// ws请求pb
message Request {
  string ModuleName = 1; // 功能模块名 例如:user、hero
  string ApiName = 2; //接口名 例如: Login、LoginOut
  google.protobuf.Any Data = 3;  // 请求参数,接口分发时动态解析
  string Sec = 4;  // 接口秘钥
}
2:gateway格式化Request为ApiRequest作为接口Request,转发到worker处理
// 接口请求pb
message ApiRequest {
  string ModuleName = 1; // 功能模块名 例如:user、hero
  string ApiName = 2; //接口名 例如: Login、LoginOut
  string Api = 3; // 拼接名 例如:user.Login
  google.protobuf.Any Data = 4;  // 请求参数,接口分发时动态解析

  string Ip = 5;  // rpcXClient地址
  string Uid = 6;  // 玩家uid
  string GatewayUrl = 7;  // gateway地址
  string WorkerUrl = 8;  // 请求的worker地址
}
3:worker处理完成之后返回ApiResponse
// 接口响应pb
message ApiResponse {
  // ResponseApi不为“”则为正常pb响应,取Data,否则就是字符串响应,取StringMsg
  int64 S = 1;  // 状态码 不为1表示请求失败
  string ErrorMsg = 2;  // 错误信息
  string ResponseApi = 3; // 响应的api 例如:user.Login
  google.protobuf.Any Data = 4;  // pb响应
  string StringMsg = 5; // 字符串响应

  string Uid = 6;  // 玩家uid
  repeated Response FirstResponse = 7;  // 需要提前返回的消息
  int64 IsCheck = 8;  // 接口检测标识 接口必须要进程接口检测,并将该字段置未1,否则调用不通过
}
4:gateway收到消息后统一转换为Response返回给客户端
// ws响应pb
message Response {
  // ResponseApi不为“”则为正常pb响应,取Data,否则就是字符串响应,取StringMsg
  int64 S = 1;  // 状态码 不为1表示请求失败
  string ErrorMsg = 2;  // 错误信息
  string ResponseApi = 3; // 响应的api 例如:user.Login
  google.protobuf.Any Data = 5;  // pb响应
  string StringMsg = 6; // 字符串响应
}

接口异常捕获

gogame_png_1

功能模块开发流程

一:一个功能模块对应一个文件夹,存放在game/module

二:接口规范

  • 1 一个接口一个go文件
  • 2 api.go是接口基类,文件名统一格式为:api_接口
  • 3 接口必须包含对应的check方法,例如,Login接口对应LoginCheck,接口必须调用Check方法进行校验并在Check方法中 将res.IsCheck置为1,否则接口调用失效
例如
func (h *Api) LoginCheck(ctx context.Context, args *pb.LoginArgs, res *rpcx.Response) {
	res.IsCheck = 1
	res.S = 1

	_sid := args.Sid
	// 区服id错误
	if !g.Config.HasSid(_sid) {
		res.S = -1
		res.ErrorMsg = "区服id错误"
		return
	}

}

// Login 登陆
func (h *Api) Login(ctx context.Context, request *pb.ApiRequest, args *pb.LoginArgs, res *rpcx.Response) {
	h.LoginCheck(ctx, args, res)
	if res.S != 1 {
		return
	}

三:fun文件存放功能相关逻辑方法,fun中一定要有init方法,服务启动会注册所有接口和注册模块到modulemanage,需要在module下的route导入模块

type ModuleUser struct {
    *base.ModuleBase
    Api
}

var User *ModuleUser

func init() {
  User = &ModuleUser{
    ModuleBase: base.NewModuleBase(),
  }
  // 手动定义接口 手动定义的接口args是any类型
  User.Api2Func = worker.Api2FuncInfo{
    // key:接口名,需要驼峰,和反射调用保持一致 value: [接口func,接口args]
    "GetInfo": worker.FmtFuncInfo(User.GetInfo, new(pb.UserGetInfoArgs)),
  }
  
  _moduleName := "user"
  // 路由注册 未定义Api2Func默认采用反射筛选符合条件的方法注册和调用
  worker.Register(_moduleName, User)
  // 模块注册
  module.Manage.User = User
  // 定时器注册 测试
  _name2Func := timer.Api2Func{
    // key就是定时器文件名 values定时器方法名
    "timer_hello": TimerHello,
  }
  timer.Register(_moduleName, _name2Func)

}
注:接口注册支持手动注册和反射注册两种,可以相互结合使用,例如上述代码,GetInfo通过func直接执行,其余未定义的接口就通过反射执行
    package module

    import (
        "fmt"
        _ "gogame/game/module/hero"
        _ "gogame/game/module/user"
    )

    func InitRoute() {
        fmt.Println()
    }
  

四:各模块之间的调用统一格式: g.M.模块名.方法名 具体实现看game/manage/modulemanage

  	// 获取所有英雄列表
	_heroList = g.M.Hero.GetHeroList("uid")
	// 获取英雄数据
	_heroInfo = g.M.Hero.GetHeroList("oid")
	
	// 创建玩家
	g.M.User.CreateUser("wnp001", "127.0.0.1", 0)
	// 获取玩家数据
	g.M.User.GetUserInfo("uid")

五:配置规范

1:配置分为两种
  • 1----功能用json(excel通过LuBan转出来的json)
  • 2----自定义的json(通过服务端自己用,放在json目录下)-----
2:一个配置文件一个go文件,有新的配置后,执行json2go.py,会自动转换成配置对应的服务go文件,文件名格式统一为"config_配置名",统一放在./manage/configmanage下
例如

功能用json----config_Hero.go

package config

import cfg "gogame/game/manage/config/structs"

type Hero struct {
  StructHandle
  *cfg.Game_hero
  NewFunc func(_buf []map[string]any) (*cfg.Game_hero, error) // 数据生成方法

  // 预处理字段定义区域---start

  // 预处理字段定义区域---end
}

func init() {
  config := &Hero{
    StructHandle: NewStructHandle("game_Hero"),
    NewFunc:      cfg.NewGame_hero,
  }
  Manage.configs.Hero = config
  RegisterConfigInterface(config.Name, config)
}

// Load 加载配置
func (self *Hero) Load(jsonData any) error {
  data, err := self.NewFunc(jsonData.([]map[string]any))
  if err != nil {
    return err
  }
  self.Game_hero = data
  self.PreConfig()
  return nil
}

// PreConfig 配置预处理
func (self *Hero) PreConfig() {

}

// ------------------------ 以上文件内容自动生成,请勿随便修改! ------------------------
// ------------------------ 下方、定义配置diy方法 ------------------------

自定义json----config_ErrorMsg.go

package config

type ErrorMsg struct {
  *JsonHandle
  // 预处理字段定义区域---start

  // 预处理字段定义区域---end
}

func init() {
  config := &ErrorMsg{
    JsonHandle: NewJsonHandle("ErrorMsg"),
  }
  Manage.configs.ErrorMsg = config
  RegisterConfigInterface(config.Name, config)
}

// Load 加载配置
func (self *ErrorMsg) Load(jsonData any) error {
  err := self.JsonHandle.Load(jsonData)
  if err != nil {
    return err
  }
  self.PreConfig()
  return nil
}

// PreConfig 配置预处理
func (self *ErrorMsg) PreConfig() {

}

// ------------------------ 以上文件内容自动生成,请勿随便修改! ------------------------
// ------------------------ 下方、定义配置diy方法 ------------------------
3:通过json2go.py生成go文件之后,在config.go中注册对应配置使用即可

gogame_png_1 gogame_png_1

六:各配置之间的调用统一格式: g.GC.配置名().方法名 具体实现看game/manage/config

	// 获取所有英雄配置
	g.GC.Hero().GetDataList()
	// 获取单个英雄配置
	g.GC.Hero().Get(101)
	// 获取star大于3星的所有英雄
	g.GC.Hero().GetListByStar(3)

	// 获取所有英雄配置
	g.GC.Item().GetDataList()
	// 获取单个英雄配置
	g.GC.Item().Get(101)
	// 获取color大于3星的所有英雄
	g.GC.Item().GetListByColor(3)

七:定时器

使用举例
每隔5秒执行一次:  */5 * * * * *
每隔1分钟执行一次:  0 */1 * * * *
每天23点执行一次:  0 0 23 * * *
每月1号凌晨1点执行一次:  0 0 1 1 * *
在26分、29分、33分执行一次:  0 26,29,33 * * * *
每天的0点、13点、18点、21点都执行一次:  0 0 0,13,18,21 * * *
1:一个定时器一个文件,文件名统一:timer_行为,放在模块目录下
2:定时器写完需要在fun文件中注册,如下图

gogame_png_1

3:定时器json添加,本服定时器添加到timer.json,跨服添加到crosstimer.json,格式如下
  {
    "timerstr": "*/2 * * * * *",    // cron格式定义
    "folder": "user",    // 所属模块
    "api": "hello",     // 接口名,注册时候func对应的key
    "args": [],     // 接口参数
    "desc": "这是测试用的"   // 定时器描述
  }
4:服务启动后,会加载timer.json和crosstimer.json中的所有定时器,效果预览:
【2022-07-19 11:03:12】 heroinit route success
【2022-07-19 11:03:12】 userinit route success

【2022-07-19 11:03:12】 **********************************************************************************************
【2022-07-19 11:03:12】 addTimer: user.hello   timerStr: */2 * * * * *  desc: 这是测试用的
【2022-07-19 11:03:12】 **********************************************************************************************
5:定时器执行完毕之后必须返回success,没有执行完毕也需要返回具体原因,效果如下
成功执行
【2022-07-19 17:05:38】 heroinit route success
【2022-07-19 17:05:38】 userinit route success

【2022-07-19 17:05:38】 **********************************************************************************************
【2022-07-19 17:05:38】 定时器加载ing...
【2022-07-19 17:05:38】 addTimer: user.timer_hello   timerStr: */2 * * * * *  desc: 这是测试用的
【2022-07-19 17:05:38】 **********************************************************************************************

【2022-07-19 17:05:40】 user timer test....hello world
【2022-07-19 17:05:40】 定时器【user.timer_hello】执行成功!!

【2022-07-19 17:05:42】 user timer test....hello world
【2022-07-19 17:05:42】 定时器【user.timer_hello】执行成功!!
错误返回
【2022-07-19 17:07:35】 heroinit route success
【2022-07-19 17:07:35】 userinit route success

【2022-07-19 17:07:35】 **********************************************************************************************
【2022-07-19 17:07:35】 定时器加载ing...
【2022-07-19 17:07:35】 addTimer: user.timer_hello   timerStr: */2 * * * * *  desc: 这是测试用的
【2022-07-19 17:07:35】 **********************************************************************************************

【2022-07-19 17:07:36】 user timer test....hello world
【2022-07-19 17:07:36】 定时器【user.timer_hello】执行失败!! msg-->测试,不允许执行
异常捕获
【2022-07-19 17:29:19】 heroinit route success
【2022-07-19 17:29:19】 userinit route success

【2022-07-19 17:29:19】 **********************************************************************************************
【2022-07-19 17:29:19】 定时器加载ing...
【2022-07-19 17:29:19】 addTimer: user.timer_hello   timerStr: */2 * * * * *  desc: 这是测试用的
【2022-07-19 17:29:19】 **********************************************************************************************

【2022/07/19 17:29:20.014】     ERROR   【timer/job.go:27】     Timer.Run error!!
api:【user.timer_hello】 args: []
err--->: 【runtime error: index out of range [0] with length 0】
goroutine 15 [running]:
runtime/debug.Stack()
        C:/Users/mayn/go1.18/go1.18/src/runtime/debug/stack.go:24 +0x65
gogame/lib/timer.(*Job).Run.func1()
        E:/wnp/go_project/gogame/lib/timer/job.go:25 +0x77
panic({0x1c1d160, 0xc0002f2dc8})
        C:/Users/mayn/go1.18/go1.18/src/runtime/panic.go:838 +0x207
gogame/game/module/user.TimerHello({0x26ff0b0?, 0x0?, 0x0?})
        E:/wnp/go_project/gogame/game/module/user/timer_hello.go:6 +0xa6
gogame/lib/timer.(*Job).Run(0xc00031a510)
        E:/wnp/go_project/gogame/lib/timer/job.go:30 +0x82
github.com/robfig/cron/v3.(*Cron).startJob.func1()
        C:/Users/mayn/go/pkg/mod/github.com/robfig/cron/v3@v3.0.1/cron.go:312 +0x6a
created by github.com/robfig/cron/v3.(*Cron).startJob
        C:/Users/mayn/go/pkg/mod/github.com/robfig/cron/v3@v3.0.1/cron.go:310 +0xad

数据层

设计思路

  • 获取数据------ redis(redis采用集群)-->mongo,游戏数据读写比例大概在37,redis的高性能读可以有效减轻mongo压力
  • redis数据存储格式------hash
  • 修改数据------玩家对数据的增删改会即可同步到redis,然后异步批量插入到mongo的model_log表,接口io延迟零体验
  • 数据同步------专门的消费服务处理model_log表中的数据,减轻数据库并发写压力
  • 数据过期------每个model默认2小时过期,每次访问model的时候会更新过期时间,model过期或玩家离线会触发清理玩家数据,过期时间每个model可单独设置,避免热数据转冷,占用redis内存
    例:
        1000个玩家产生了1万条数据,正常会与mongo产生1万次io
        model类会整合这1万条数据,insertMany入库到mongo的model_log表,只进行1次io
        消费服务会监听mongo的model_log表,有数据就会即时消费掉这些数据
  • 注:model类的insertMany和消费服务都是1s执行1次,就是说极限会丢失1s的数据
  • 后续针对某些访问特别频繁的数据(比如玩家的userinfo),准备引入内存机制,访问机制改为:内存-->redis-->mongo

一:一个mongo的表对应一个model文件,文件命名统一“model_表名”,存放在game/manage/model下

例如:
  • userinfo表对应的model类,以下是代码直接复制,修改表名即可
  • ModelBase是model基类,NewModelBase传入的参数依次是(表名,玩家数据唯一key)
    例如:
        userinfo表的唯一key是uid,一个玩家只有一条数据
        hero表的唯一key是_id,一个玩家有多个英雄
```go
type UserInfo struct {
	*ModelBase
}

func init() {
	model := &UserInfo{
		ModelBase: NewModelBase("userinfo", "uid", SetExpireTime(time.Hour*6)),
	}
	Manage.models.UserInfo = model
}

二:数据相关操作的方法全部定义在model类文件中,model类文件定义完成之后需要在model.go中注册对应的model类

// 这里的单独使用 g.GC会导致循环引用
var (
	GC = config.Manage
	M  = module.Manage
	C  = lib.Common
)

type models struct {
	UserInfo *UserInfo
	Hero     *Hero
}

三:在model类中使用g.GC g.M g.C,全部替换为GC M C,原因是避免循环引用

四:方法使用

1:获取一条数据 Get
// GetInfo 获取英雄数据  外部调用就是g.DATA.Hero.GetInfo(oid)
func (self *Hero) GetInfo(id string) *pb.ModelHero {
    heroInfo := new(pb.ModelHero)
    self.Get(id, heroInfo)

return heroInfo
}
2:获取列表数据 GetList
// GetList 获取玩家英雄列表  外部调用就是g.DATA.Hero.GetList(uid)
func (self *Hero) GetList(uid string) []*pb.ModelHero {
	var heroList []*pb.ModelHero
	self.ModelBase.GetList(uid, &heroList)

    // 测试,没有英雄数据增加几个
    if len(heroList) == 0 {
      _hid2Hero := self.AddHero(uid, 11001, 11002, 11003)
      for _, h := range _hid2Hero {
            heroList = append(heroList, h)
      }
    }

	return heroList
}
3:修改数据 Set
// 修英雄的等级修改为666级
_setData := map[string]any{
	"lv": 666
}
g.DATA.Hero.Set("62df6524ae0dd951b1daaafc", _setData)
4:删除数据 Remove
g.DATA.Hero.Remove("62df6524ae0dd951b1daaafc")
5:插入一条数据 InsertOne
g.DATA.Hero.InsertOne("62df6524ae0dd951b1daaafc")

例如
// AddHero 增加一个英雄
func (self *Hero) AddHero(uid string, hid int32) *pb.ModelHero {
    _heroInfo := M.Hero.GetDefHeroInfo(uid, hid)
    // 英雄不存在
    if _heroInfo == nil {
        return nil
    }
    _heroInfo.Id = C.GetObjectId()
    self.InsertOne(uid, _heroInfo)
    return _heroInfo
}
6:插入多条数据 InsertMany
g.DATA.Hero.InsertMany([]any)

例如
_heroList := []any{
	g.M.Hero.GetDefHeroInfo(uid, 11001),
	g.M.Hero.GetDefHeroInfo(uid, 11001),
	g.M.Hero.GetDefHeroInfo(uid, 11001),
}
g.DATA.Hero.InsertMany(uid, _heroList)

空文件

简介

go实现的游戏逻辑框架 展开 收起
Go
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Go
1
https://gitee.com/hyhhui/gogame.git
git@gitee.com:hyhhui/gogame.git
hyhhui
gogame
gogame
master

搜索帮助