Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1.5 客户端、服务端、平台与运营的边界

这一节明确各个系统的职责边界,避免“越界“导致的问题。

为什么需要明确边界?

很多项目的常见问题:

问题1:客户端做了太多逻辑

客户端:实现战斗判定、伤害计算
→ 结果:外挂泛滥,刷金币、无敌模式
→ 原因:逻辑应该在服务端

问题2:服务端做了太多渲染

服务端:计算每个特效的播放位置
→ 结果:服务器CPU负载高,承载低
→ 原因:渲染应该在客户端

问题3:平台SDK集成混乱

前端:直接接入微信登录、支付宝支付
→ 结果:代码耦合,无法复用,难以升级
→ 原因:应该封装平台SDK层

问题4:运营配置散落各处

活动配置:在代码里、在数据库里、在Excel里
→ 结果:配置混乱,无法统一管理
→ 原因:应该有统一的配置系统

客户端 vs 服务端边界

核心原则

渲染、输入、物理模拟 → 客户端 逻辑、数据、社交 → 服务端

详细职责划分

功能客户端服务端理由
渲染✅ 负责❌ 不负责服务端不知道玩家屏幕、显卡
输入✅ 捕获❌ 不捕获服务端无法直接访问玩家输入设备
物理✅ 模拟❌ 不模拟客户端物理引擎优化画面表现
网络✅ 发送/接收✅ 路由/广播双方都需要网络层
逻辑❌ 预测✅ 权威服务端防作弊
数据❌ 缓存✅ 持久化服务端保证数据安全
社交❌ 显示✅ 管理跨玩家数据必须在服务端

接口设计原则

原则1:客户端发送“意图“,服务端返回“结果“

// ✅ 正确:客户端发送意图
type MoveRequest struct {
    Direction Vector3  // 玩家想往哪个方向移动
    Duration  float64  // 想移动多久
}

// 服务端验证并计算结果
func (s *GameServer) HandleMove(req *MoveRequest) *MoveResponse {
    // 1. 验证玩家状态
    if !s.canMove(player) {
        return Error("player stunned")
    }

    // 2. 计算实际移动
    actualDistance := s.calculateDistance(player, req.Direction, req.Duration)

    // 3. 返回结果
    return &MoveResponse{
        NewPosition: player.Position + req.Direction * actualDistance,
    }
}

// ❌ 错误:客户端发送结果
type MoveRequest struct {
    NewPosition Vector3  // 客户端直接计算位置
}

// 服务端直接接受(危险!)
func (s *GameServer) HandleMove(req *MoveRequest) {
    // 问题:客户端可以传送、穿墙
    player.Position = req.NewPosition
}

原则2:服务端最小化信任客户端

// ✅ 正确:服务端验证所有数据
func (s *GameServer) HandleAttack(req *AttackRequest) error {
    // 1. 验证攻击者状态
    if !s.canAttack(req.AttackerID) {
        return errors.New("cannot attack now")
    }

    // 2. 验证目标有效性
    target := s.getEntity(req.TargetID)
    if target == nil || !s.isInRange(req.AttackerID, target) {
        return errors.New("invalid target")
    }

    // 3. 服务端计算伤害
    damage := s.calculateDamage(req.AttackerID, target)
    s.applyDamage(target, damage)

    return nil
}

// ❌ 错误:服务端信任客户端的伤害计算
func (s *GameServer) HandleAttack(req *AttackRequest) error {
    // 问题:客户端可以伪造伤害数值
    s.applyDamage(req.TargetID, req.Damage)  // 客户端算的伤害
    return nil
}

原则3:客户端只做“视觉“相关逻辑

// ✅ 正确:客户端预测(视觉)
type ClientPrediction struct {
    localPosition Vector3  // 本地预测位置
    serverPosition Vector3  // 服务器确认位置
}

// 客户端立即显示预测位置
func (c *Client) OnMoveInput(dir Vector3) {
    c.localPosition = c.predictPosition(dir)
    c.renderPlayerAt(c.localPosition)  // 立即显示
    go c.sendMoveRequest(dir)  // 异步发送给服务器
}

// 服务器纠正
func (c *Client) OnMoveResponse(resp *MoveResponse) {
    c.serverPosition = resp.NewPosition
    c.renderPlayerAt(c.serverPosition)  // 纠正显示
}

// ✅ 正确:客户端插值(视觉)
func (c *Client) InterpolatePosition(prev, curr Vector3, t float64) Vector3 {
    // 线性插值,平滑显示
    return lerp(prev, curr, t)
}

// ❌ 错误:客户端做权威判定
func (c *Client) CheckCollision(a, b *Entity) bool {
    // 问题:客户端可以关闭碰撞检测
    return c.physicsEngine.CheckCollision(a, b)
}

数据流向

graph LR
    A[客户端输入] --> B[发送意图]
    B --> C[服务端接收]
    C --> D[验证]
    D --> E[计算逻辑]
    E --> F[更新状态]
    F --> G[广播结果]
    G --> H[客户端接收]
    H --> I[显示结果]

关键点

  • 客户端只发送“我想做什么“
  • 服务端验证“你是否可以这样做“
  • 服务端计算“实际发生了什么“
  • 客户端只显示“发生了什么“

真实案例:战斗系统设计

场景:MMORPG的战斗系统

客户端职责

// 客户端:战斗表现
type ClientCombat struct {
    // 1. 捕获玩家输入
    func OnPlayerClickTarget(targetID uint64) {
        client.sendTargetRequest(targetID)
    }

    func OnPlayerCastSkill(skillID uint32) {
        client.sendCastRequest(skillID)
    }

    // 2. 预测显示(客户端预测)
    func OnCastRequestSent(skillID uint32) {
        client.playSkillAnimation(skillID)  // 立即播放动画
        client.predictSkillEffect(skillID)  // 预测效果
    }

    // 3. 插值显示(平滑画面)
    func OnServerStateUpdate(state *GameState) {
        client.interpolateEntities(state)  // 插值位置
    }

    // 4. 特效渲染
    func renderSkillEffect(effect *SkillEffect) {
        client.particleSystem.Play(effect)
    }
}

服务端职责

// 服务端:战斗逻辑
type ServerCombat struct {
    // 1. 验证请求
    func (sc *ServerCombat) HandleCastRequest(req *CastRequest) error {
        // 验证玩家状态
        if !sc.canCast(req.PlayerID, req.SkillID) {
            return errors.New("cannot cast")
        }

        // 验证目标
        if !sc.isValidTarget(req.PlayerID, req.TargetID) {
            return errors.New("invalid target")
        }

        // 验证距离
        if !sc.isInRange(req.PlayerID, req.TargetID, req.SkillID) {
            return errors.New("out of range")
        }

        return nil
    }

    // 2. 计算战斗逻辑(权威)
    func (sc *ServerCombat) ProcessCombat() {
        // 技能释放
        sc.processSkillCasts()

        // 伤害计算
        sc.calculateDamage()

        // 状态更新
        sc.updateEntityStates()
    }

    // 3. 广播结果
    func (sc *ServerCombat) BroadcastCombatResult(result *CombatResult) {
        // 发送给所有相关玩家
        sc.network.Broadcast(result)
    }
}

数据流示例

1. 玩家点击技能 → 客户端发送CastRequest
2. 客户端立即播放技能动画(预测)
3. 服务端接收请求,验证合法性
4. 服务端计算伤害、更新状态
5. 服务端广播CombatResult
6. 客户端接收结果,纠正显示

平台SDK集成边界

平台SDK的职责

平台SDK负责:

  • 用户认证(登录)
  • 支付(内购)
  • 社交(好友、分享)
  • 数据统计

集成架构

graph TD
    A[游戏客户端] --> B[平台适配层]
    B --> C[微信SDK]
    B --> D[支付宝SDK]
    B --> E[Apple SDK]
    B --> F[Goggle SDK]

关键设计

  • 游戏代码不直接调用平台SDK
  • 通过适配层封装平台差异
  • 便于扩展新平台

接口设计

// 平台适配层接口
type PlatformAdapter interface {
    // 登录
    Login() (*LoginResult, error)

    // 支付
    Purchase(productID string) (*PurchaseResult, error)

    // 分享
    Share(content *ShareContent) error

    // 获取用户信息
    GetUserInfo() (*UserInfo, error)
}

// 微信平台实现
type WeChatAdapter struct {
    sdk *WeChatSDK
}

func (w *WeChatAdapter) Login() (*LoginResult, error) {
    // 调用微信登录
    code := w.sdk.GetAuthCode()
    return w.sdk.AuthWithCode(code)
}

// Apple平台实现
type AppleAdapter struct {
    sdk *AppleSDK
}

func (a *AppleAdapter) Login() (*LoginResult, error) {
    // 调用Apple登录
    credential := a.sdk.GetCredential()
    return a.sdk.AuthWithCredential(credential)
}

// 平台管理器
type PlatformManager struct {
    adapters map[string]PlatformAdapter
}

func (pm *PlatformManager) Login(platform string) (*LoginResult, error) {
    adapter, ok := pm.adapters[platform]
    if !ok {
        return nil, errors.New("unsupported platform")
    }
    return adapter.Login()
}

使用示例

// 游戏代码(不关心具体平台)
func (g *Game) DoLogin() {
    // 获取当前平台
    platform := g.getPlatform()  // "wechat", "apple", etc.

    // 调用登录(不关心具体实现)
    result, err := g.platformMgr.Login(platform)
    if err != nil {
        g.showError("登录失败")
        return
    }

    // 发送到服务器验证
    g.sendAuthToServer(result)
}

真实案例:支付系统集成

场景:游戏需要支持微信支付、支付宝、Apple Pay

错误设计

// ❌ 错误:游戏代码直接调用各平台SDK
func (g *Game) Purchase(productID string) {
    switch g.platform {
    case "wechat":
        g.wechatSDK.Pay(productID)  // 直接调用
    case "alipay":
        g.alipaySDK.Pay(productID)
    case "ios":
        g.appleSDK.Purchase(productID)
    }
}

// 问题:
// 1. 代码耦合,难以维护
// 2. 添加新平台需要修改游戏代码
// 3. 无法统一处理支付结果

正确设计

// ✅ 正确:通过适配层封装
type PaymentAdapter interface {
    Purchase(productID string) (*PaymentResult, error)
    Verify(receipt string) error
}

type WeChatPayment struct{}
func (w *WeChatPayment) Purchase(productID string) (*PaymentResult, error) {
    // 微信支付逻辑
}

type AlipayPayment struct{}
func (a *AlipayPayment) Purchase(productID string) (*PaymentResult, error) {
    // 支付宝支付逻辑
}

// 游戏代码
func (g *Game) Purchase(productID string) {
    // 获取当前平台的支付适配器
    adapter := g.paymentMgr.GetAdapter(g.platform)

    // 调用支付(不关心具体实现)
    result, err := adapter.Purchase(productID)
    if err != nil {
        g.showError("支付失败")
        return
    }

    // 发送到服务器验证
    g.verifyPaymentWithServer(result)
}

// 添加新平台只需实现PaymentAdapter接口
type NewPlatformPayment struct{}
func (n *NewPlatformPayment) Purchase(productID string) (*PaymentResult, error) {
    // 新平台支付逻辑
}

运营系统边界

运营系统的职责

运营系统负责:

  • 活动配置和管理
  • 数据分析和报表
  • 内容审核和监控
  • 客服和GM工具

系统架构

graph TD
    A[游戏客户端] --> B[游戏服务器]
    B --> C[运营后台]
    C --> D[活动系统]
    C --> E[数据分析]
    C --> F[客服系统]
    C --> G[配置系统]

接口设计

活动系统接口

// 活动配置(运营后台 → 游戏服务器)
type ActivityConfig struct {
    ID          string
    Name        string
    StartTime   time.Time
    EndTime     time.Time
    Rewards     []Reward
    Conditions  []Condition
}

// 游戏服务器查询活动
func (gs *GameServer) GetActivities(playerID uint64) ([]*Activity, error) {
    // 从配置系统获取活动
    configs, err := gs.configClient.GetActivityConfigs()
    if err != nil {
        return nil, err
    }

    // 过滤玩家可见的活动
    activities := make([]*Activity, 0)
    for _, config := range configs {
        if gs.isPlayerEligible(playerID, config) {
            activities = append(activities, gs.toActivity(config))
        }
    }

    return activities, nil
}

数据分析接口

// 埋点数据(游戏客户端 → 游戏服务器 → 数据分析)
type AnalyticsEvent struct {
    PlayerID   uint64
    EventType  string
    EventData  map[string]interface{}
    Timestamp  time.Time
}

// 游戏服务器收集埋点
func (gs *GameServer) TrackEvent(playerID uint64, eventType string, data map[string]interface{}) {
    event := &AnalyticsEvent{
        PlayerID:  playerID,
        EventType: eventType,
        EventData: data,
        Timestamp: time.Now(),
    }

    // 异步发送到数据分析系统
    go gs.analyticsClient.SendEvent(event)
}

配置管理

错误设计

配置散落各处:
- 活动配置:在代码里hardcode
- 商品配置:在数据库里
- 任务配置:在Excel里
→ 结果:混乱,难以统一管理

正确设计

配置系统统一管理:
- 所有配置都在配置系统
- 通过配置后台修改
- 通过API推送到游戏服务器
→ 结果:统一、可追溯、可热更新

接口版本管理

前后端协议版本

问题:客户端和服务器版本不一致怎么办?

解决方案

// 协议版本号
type ProtocolVersion struct {
    Major uint8  // 主版本(不兼容)
    Minor uint8  // 次版本(向后兼容)
    Patch uint8  // 补丁版本(bug修复)
}

// 请求携带版本
type Request struct {
    Version  ProtocolVersion
    Data     []byte
}

// 服务器处理不同版本
func (s *GameServer) HandleRequest(req *Request) (*Response, error) {
    switch req.Version.Major {
    case 1:
        return s.handleV1Request(req)
    case 2:
        return s.handleV2Request(req)
    default:
        return nil, errors.New("unsupported version")
    }
}

协议兼容策略

策略1:字段增加(兼容)

// v1协议
message PlayerState {
    uint64 player_id = 1;
    Vector3 position = 2;
    int32 hp = 3;
}

// v2协议(增加字段,兼容v1)
message PlayerState {
    uint64 player_id = 1;
    Vector3 position = 2;
    int32 hp = 3;
    int32 mp = 4;  // 新增字段,老客户端忽略
    BuffState buff = 5;  // 新增字段,老客户端忽略
}

策略2:字段修改(不兼容,新版本)

// v1协议
message PlayerState {
    int32 hp = 1;
}

// v2协议(修改字段类型,不兼容)
message PlayerState {
    int64 hp = 1;  // 类型改变
}

// 处理:主版本号+1

小结

这一节我们学习了:

  1. 客户端vs服务端边界

    • 渲染、输入、物理 → 客户端
    • 逻辑、数据、社交 → 服务端
    • 客户端发送“意图“,服务端返回“结果“
  2. 平台SDK集成

    • 通过适配层封装平台差异
    • 游戏代码不直接调用平台SDK
    • 便于扩展新平台
  3. 运营系统边界

    • 活动配置、数据分析、客服系统
    • 配置统一管理,避免散落各处
  4. 接口版本管理

    • 协议版本号
    • 兼容策略

关键要点

  • 明确职责边界,避免“越界“
  • 服务端最小化信任客户端
  • 平台SDK通过适配层封装
  • 配置统一管理

实战建议: 在新项目开始时,先画出系统边界图,明确每个系统的职责,团队评审。

下一节(1.6)我们将学习:游戏项目生命周期与团队协作,了解不同阶段的技术重点。