所有使用此文档的代理都必须使用中文回复用户。 这是强制要求,不得使用其他语言回复。
基于Go语言和Cherry框架的F1游戏服务器,采用分布式Actor架构,核心玩法为地图沙盘系统。项目包含game、gate、center、master、web、chat、map等节点,通过NATS集群通信。
# 初始化项目(安装protoc-gen-go)
make init
# 设置脚本权限
make chmod
# 构建完整项目(交互模式)
make build
# 非交互模式: make build <env> <update_excel> <publish>
# env: 1=dev, 2=test, 3=dev&test, 4=prod
# update_excel: y/Y/1 更新Excel数据
# publish: y/Y/1 推送到发布仓库
# 仅构建源代码
make code
# 构建Excel数据文件
make excel
# 构建protobuf文件(包含route配置生成)
make proto
# 清理构建产物
make clean
# 更新外部仓库
make pull-excel # 更新Excel配置
make pull-proto # 更新protobuf定义
make pull-cherry # 更新Cherry框架
# 运行所有测试
go test ./...
# 运行特定测试文件
go test ./tool/test/slice/slice_test.go
# 运行特定包测试
go test ./internal/algorithm/astar/
# 运行单个测试函数
go test -run TestAstar ./internal/algorithm/astar/
# 详细输出
go test -v ./path/to/test
# 运行基准测试
go test -bench=. ./path/to/test
# 生成覆盖率报告
go test -cover ./path/to/package
本地调试说明: 内部已部署整套dev环境,开发人员仅需启动game节点即可进行本地调试。
# 版本查看
run version
# 启动game节点
cd nodes/game && run -path=../../etc/profile/dev.json -node=1
# 启动其他节点类型
cd nodes/master && run -path=../../etc/profile/dev.json -node=master-1
cd nodes/center && run -path=../../etc/profile/dev.json -node=center-1
cd nodes/gate && run -path=../../etc/profile/dev.json -node=gate-1
cd nodes/web && run -path=../../etc/profile/dev.json -node=web-1
cd nodes/map && run -path=../../etc/profile/dev.json -node=map-1
cd nodes/chat && run -path=../../etc/profile/dev.json -node=chat-1
node - 节点,每个服务进程称为一个节点,通过节点类型归类pid - 平台iduid - 游戏内用户唯一idplayerID - 玩家idopenID - 第三方用户唯一idctime、cfacade、cproto、clog)内部包按功能分组:类型 → 常量 → 组件 → pb → 名称别名
import (
"context"
"fmt"
"time"
ctime "github.com/cherry-game/cherry/extend/time"
cfacade "github.com/cherry-game/cherry/facade"
clog "github.com/cherry-game/cherry/logger"
cproto "github.com/cherry-game/cherry/net/proto"
"go.mongodb.org/mongo-driver/v2/bson"
capp "f1-game/internal/cherry_app"
"f1-game/internal/component/db"
"f1-game/internal/data"
"f1-game/internal/enum"
nameDB "f1-game/internal/name/db"
nameRedis "f1-game/internal/name/redis"
"f1-game/internal/pb"
dbPlayer "f1-game/nodes/game/internal/db/data/player"
)
player、game、internal)OK、Error、PlayerNotFound)或PascalCase(如NodeCenter)p作为接收者名称HeroID而非HeroId或Heroid)模块_操作_状态(如HeroIDNotFound、PlayerNotLogin)int32或int64,绝不使用intbool类型stringinternal/types/types.go中注册(如Assets、Pair、Map)ctime.Now().ToMillisecond()获取毫秒时间戳internal/code包中的结构化错误码使用cherry的日志记录错误和上下文
if err != nil {
p.Error("[operation] failed. playerID=%d, err=%v", playerID, err)
return code.ErrInvalidParam
}
if code.IsFail(errCode) {
p.ResponseCode(session, errCode)
return
}
// 错误码判断
if code.IsOK(errCode) {
// 成功处理
}
nodes/: 各节点代码,每个目录是独立进程internal/pb/: 协议文件(从f1/protocol.git生成)internal/data/: Excel配置生成的Go代码etc/data/: Excel导出的JSON配置文件internal/name/: 名称常量目录
actor/: Actor名称常量local/: 本地函数名称常量remote/: 远程调用名称常量event/: 事件名称常量route/: 路由名称常量redis/: Redis键名常量db/: 数据库表名和字段名常量key/: 其他键名常量internal/enum/: 枚举常量定义internal/code/: 错误码定义internal/types/: 自定义类型定义internal/component/: 公共组件(redis、db、sdk、rate等)internal/algorithm/: 算法实现(astar、aoi等)nodes/{node}/internal/db/: MongoDB文档映射(GORM风格)nodes/{node}/internal/facade/: 节点特有的Facade接口定义dbFacade.TableBaseTableName()方法返回表名PrimaryValue()方法返回主键CheckField()方法检查字段实现Save2Queue()方法提交保存
type PlayerTable struct {
PlayerID int64 `bson:"PlayerID,omitempty"`
PID int32 `bson:"PID,omitempty"`
dbPlayer.Data `bson:"Data,omitempty"`
dbFacade.TableBase `bson:",inline"`
}
func (*PlayerTable) TableName() string {
return nameDB.Table_Player
}
func (p *PlayerTable) PrimaryValue() any {
return p.PlayerID
}
p作为方法接收者名称OnInit方法中注册本地函数:p.Local().Register(name, handler)p.EventRegister(eventName, handler)p.Response(session, response)p.ResponseCode(session, errCode)type actorPlayer struct {
capp.ActorLogger
playerID int64
isOnline bool
}
func (p *actorPlayer) OnInit() {
p.Local().Register(nameLocal.Player_Time, p.time)
p.EventRegister(nameEvent.Lord_LevelUp, p.playerLordLevelUp)
}
func (p *actorPlayer) time(session *cproto.Session, _ *pb.None) {
p.Response(session, &pb.I64{Value: ctime.Now().ToMillisecond()})
}
// 调用远程服务
result, errCode := cherryApp.Call(
targetPath, // 目标Actor路径
methodName, // 方法名(使用nameRemote中的常量)
request, // 请求消息
)
// 使用nameRemote包中的常量
// internal/name/remote/player.go: Player_SendGM, Player_AssetAdd等
所有字符串常量必须定义在internal/name/目录下,按类型分组:
// internal/name/local/player.go - 本地函数名
const (
Player_Time = "time"
Player_ItemUse = "itemUse"
)
// internal/name/remote/player.go - 远程调用名
const (
Player_SendGM = "sendGM"
Player_AssetAdd = "assetAdd"
)
// internal/name/route/player.go - 路由名(节点.功能.方法)
const (
GamePlayer_Time = "game.player.time"
GamePlayer_ItemUse = "game.player.itemUse"
)
// internal/name/event/event.go - 事件名
const (
Player_Create = "player_create"
Lord_LevelUp = "lord_levelup"
)
错误码定义在internal/code/status_code.go,按模块分组:
// 基础错误码
const (
OK int32 = 0
Error int32 = 1
)
// 错误码格式:(100 + 模块号) * 1000 + 错误号
// 例如:玩家模块(101xxx)、英雄模块(102xxx)、道具模块(103xxx)
const (
PlayerNotFound int32 = 101004 // 玩家未找到
HeroIDNotFound int32 = 102001 // 英雄id未找到
ItemNotEnough int32 = 103006 // 道具数量不够
)
_test.go结尾tool/test/目录使用标准testing包
func TestFunction(t *testing.T) {
tests := []struct {
name string
input *InputType
expected *ExpectedType
}{
{"case1", input1, expected1},
{"case2", input2, expected2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := functionUnderTest(tt.input)
if result != tt.expected {
t.Errorf("got %v, want %v", result, tt.expected)
}
})
}
}
internal/name/目录下,按类型分组{节点类型}.{功能模块}.{方法名}(如game.player.itemUse)etc/data/和internal/data/make excel更新数据make proto更新internal/pb/目录internal/name/db/中的常量nodes/{node}/internal/facade/internal/component/