查看: 1477|回复: 1
收起左侧

[游戏开发] 开发笔记:游戏逻辑模块组织及数据同步

[复制链接]

[游戏开发] 开发笔记:游戏逻辑模块组织及数据同步[复制链接]

泰课_robin 发表于 2017-8-31 19:52:07 [显示全部楼层] |只看大图 回帖奖励 |倒序浏览 |阅读模式 回复:  1 浏览:  1477
文/++阿联酋长
1 W. ^9 b( h! M2 N" m( _# L4 E  j2 e* P; B- h
一个游戏根据功能可以划分为多个不同的模块,如金钱、背包、装备、技能、任务、成就等。按照软件工程的思想,我们希望分而治之单独实现不同的模块,再将这些模块组合在一起成为一份完整的游戏。但现实是残酷的,不同模块之间往往有千丝万缕的联系,比如购买背包物品会需要扣金币、打一个副本会完成任务,完成任务又会奖励金币和物品,金币的增加又导致一个成就达成。于是我们虽然在不同的类或不同的文件中来实现各个模块,却免不了模块间的交叉引用和互相调用,最后混杂不堪,任何一点小修改都可以导致牵一发而动全身。, N9 M6 j% s5 Y$ E

% K* Q: R* g! H: c为了后面说明方便,我们考虑这样一个小型游戏系统:总共有3个模块,分别是金钱、背包、任务。购买背包物品需要消耗金币,卖出背包物品可得到金币,金币增加到一定数额后会导致某个任务的状态变为完成,完成任务可获得物品和金币。这3个模块的调用关系如图。
3 r4 C* b6 Q, _
" J* q, L/ _: L% Q4 `

开发笔记:游戏逻辑模块组织及数据同步

开发笔记:游戏逻辑模块组织及数据同步
  
) s2 G7 v4 L; z& x
% S* w5 r# I% w7 T3 w& V2 ]) G
- k" V0 ?' q3 \: ]8 K

6 D% R4 ^5 m# S+ n首先我们把模块的数据和逻辑分离,借鉴经典的MVC模式,数据部分叫作Model,逻辑部分叫作Controller。如此一来,游戏功能部分就被划分出来了两个不同的层次,Controller处于较高的层次上,可以引用一个或者多个Model。Model层专心处理数据,对上层无感知。每个Model都是完全独立的模块,不引用任何Controller或Model,不依赖于其他任何对象,可以单拿出来进行单元测试。) ]4 c, k& x7 i1 y

- E) L. k6 G% o1 ?1 F" O4 B

开发笔记:游戏逻辑模块组织及数据同步

开发笔记:游戏逻辑模块组织及数据同步
  8 s- |6 y; D) \6 N

& r$ A! l$ S( r  S8 r
2 P# f+ x$ e) b, x; _7 H
对于我们的例子,每个模块提供的接口列举如下:: T# `  X8 [' y% M& V9 _* t4 h: O
3 X" ]6 V! {1 `% N$ T0 _
BagModel:获取物品数量,增加物品,扣除物品
8 {# @, z9 D! L' v/ M0 P' w0 D) u( w; |7 A2 m- `
MoneyModel:获取金币数量,增加金币,扣除金币5 c. c! H8 |+ `/ c% E" u

1 a. _" X$ h# ^" C3 MTaskModel:增加任务,删除任务,标记任务为完成6 S, @5 V9 _' `- d
& q6 ?5 I9 k* n: j. g+ |( }4 y+ ^/ V0 J# i
BagController:购买物品,卖出物品
2 _3 n. p" {3 Q+ Q0 a5 v7 S' l  ]$ D3 q3 B, n/ z/ p' c& b
TaskController:完成任务
& Z" U" z) S- ]5 [+ r( H5 e4 Y
' c6 T* n! v6 o6 {购买或卖出物品时,由BagController进行或操作校验,随后调用BagModel和MoneyModel完成数据修改。完成任务时,由TaskController调用各个模块。; L' ]- R5 q& q, N$ M

( T  a& @: n  u/ U1 |7 U# n" L: W现在唯一的问题是,既然MoneyModel不引用其他模块,那么在金币增加时如何告知任务模块去完成任务呢?这里我们需要引入一个管理依赖的利器:观察者模式。
# u% g: R  y7 U& X! U( r1 H+ E2 k: i4 T
具体使用方式是把Model实现为一个Subject,对某个Model的数据变化感兴趣的Controller实现为对应的Observer。我们的例子中,MoneyModel是Subject,在金币数量变化时通知所有已注册的Observer;TaskController是MoneyModel的一个Observer,在初始化时向MoneyModel注册。; b# u* Q- z! [! U1 @' p, r3 B
" v% ^  }# s% R3 a3 ~+ A

开发笔记:游戏逻辑模块组织及数据同步

开发笔记:游戏逻辑模块组织及数据同步
  
' [6 K# W# m2 h# j5 c" e' Z( T$ J+ e: j# V

/ F. `1 u+ S9 V( }" W1 s" b
, }7 T5 U* o4 s7 v$ q  A/ q
注意图中由MoneyModel指向TaskController的虚线箭头,代表MoneyModel数据变化时会去通知TaskController,用虚线是因为MoneyModel并不依赖于TaskController(只依赖于Observer接口)。同样BagModel也可以提供背包物品变化的Subject,如果新加一个任务是要求某物品的数量达某个值,那么TaskController可向BagModel注册,这样在物品变化时就能得到通知了,图中也画出了这条虚线。+ a8 c# V9 a9 Z" ^

' S1 L/ l% u0 S0 m7 b对观察者模式不熟悉的读者朋友可以自行查阅资料, 本文的重点并不是介绍设计模式。这里简单提示一下观察者模式的精髓:当某模块调用其他模块时就产生了依赖,这时可以不直接去调用,而是转而实现一个机制,这个机制就是让其他模块告诉自己他们需要被调用。最后调用的流程没变,变化的是依赖关系。) C& F8 C& S; H! g* q3 e
! u- d6 G, e3 k# y
在客户端情况要更复杂一些,实际上加入UI后,我们的模块设计就成经典的MVC,这也是我们为什么把数据模块和逻辑模块分别叫Model和Controller的原因。
# H. `: C9 q; {) V
/ e7 }* C" L( T7 m' B9 w/ E% k: L

开发笔记:游戏逻辑模块组织及数据同步

开发笔记:游戏逻辑模块组织及数据同步
  3 D/ E# E) _0 b. O8 T
) G# Y/ T( ^# t( z0 E
3 R7 o# c# R* u' d6 {2 m& A
这里只画出了背包模块。这里的System API指与游戏运行平台相关的一些接口,可能是操作系统API、引擎API、图形库API等等。View模块和Model模块地位相当,只处理显示而不管游戏功能,需要显示的数据都是由Controller提供的。对于能输入的View同样采用观察者模式,点击等事件发生时通知其他模块(而不是直接调用),注意图中由BagView指向BagController的虚线箭头。
# K+ x9 ]2 _  ^; d
5 F+ J2 n1 |9 W( C$ [下面介绍数据同步的设计。. e4 k, d6 _( `0 P9 Y

/ n5 W! \) |# [4 \9 Q首先对于网络游戏,客户端所展示的数据是服务器传送过来的。当玩家操作导致数据发生变化时,最好也由服务器更新给客户端。曾经接手过一个项目,很多操作的结果都是客户端先算出来的,于是各种逻辑都是服务器和客户端各实现一遍,很容易两边的数据就不一致了,很让人头疼。5 o4 N& d6 i0 |6 D7 Y5 p( A

3 `* m8 w6 j( b& f, f$ ^" D所以我们的同步思路是当客户端向服务器发起一个请求时,服务器将所有变化的数据同步给客户端,客户端收到服务器的返回后再更新数据,绝不私自改动数据。在这个指导思想下,我们消息包结构是这样的(以物品卖出举例):
* M! M# f/ M" i8 U: M+ @
! G0 ?3 O1 R0 c6 Z7 k- S
  • message BagItemSellCG {
  • optional int32 id = 1;
  • opitnoal int32 count = 2;
  • }
  • message BagItemSellGC {
  • optional int32 result = 1;
  • optional Sync sync = 2;
  • opitonal BagItemSellCG postback = 3;, o; ]; d8 u1 T* Y
    }
    4 y& k) i1 l! K- p* I8 f

& z% C: d9 S. Y  _7 W) |复制代码
1 H% C, l# ]* F# {! k7 ~3 u7 b+ z
" ?1 Z% ]7 K6 b1 l2 i; a服务器向客户端返回的消息几乎总是包含3个字段。result为操作结果可能是0或者错误码,sync中包含了所有的数据更新,postback将客户端的请求消息原封不动返回去,便于客户端进行界面更新或友好提示。
& P6 _+ T  y) K, A4 l4 i
$ T; h$ v! j! Qsync是一个比较复杂的message,包含了所有需要更新的Model的数据。感谢Protocol Buffer的optional选项,大多数情况下我们发送的数据只是其中很小的一部分。; W- }" E2 e5 }, l

! A# _$ U( K9 |+ A5 ?先来看服务器端消息处理和同步的设计。
$ g  `4 I" ?0 W7 m% R
* D. e! i$ P9 G6 t5 D

开发笔记:游戏逻辑模块组织及数据同步

开发笔记:游戏逻辑模块组织及数据同步
  
7 w. o0 K& }7 d5 b9 @2 G( e' F5 l7 ^* e6 e% ^7 \
) G. X. J/ v1 q
如图所示,我们在Model和Controller之上新加了一个Handler接口层。Handler负责解析消息包,调用Controller处理消息包,在必要的时候调用SyncController构建同步数据,最后打包成消息返回给客户端。
7 N; D0 \) x/ `9 r8 Y' ], _- L2 ?: b# ]( r/ N
每个Model在管理数据的基础上会维护变化数据的集合,对于简单的Model比如MoneyModel就是一个bool脏标记,而BagModel则维护变化物品id的集合。变化数据列表在同步之后清除。; U1 C$ E, o' b8 _' d" d% e
% _' g9 y; g+ d. g  t1 y
客户端的结构是类似的。
+ k# Q5 K. b$ l2 J7 k4 b& Y  j
0 j& C# t: o% l+ }! g

开发笔记:游戏逻辑模块组织及数据同步

开发笔记:游戏逻辑模块组织及数据同步
  
& J% T$ P+ a5 ?7 X

. ^; M( `9 y& U9 M: S/ C+ W* O1 H与服务器的区别就在于SyncController是负责调用Model更新数据,每个Model都实现数据更新接口。注意除SyncController之外,其他Controller只能读取Model而不能改变其数据,这样就保证了所有数据一定是从服务器同步的。! H4 \6 h4 I) K2 q  T$ x: p
! L! K$ P5 y! ~1 F: C0 }" m
最后我想以出售物品为例子完整走一遍流程。从客户端进行操作开始,到请求发到服务器,最后再返回客户端更新数据和界面。完整的图比较复杂,混在一起基本上没法看了,只好删掉了客户端的任务模块……- B- Z( O2 a( Q; x$ R/ {) D9 D% k

) Z6 i4 g- U$ K9 w

开发笔记:游戏逻辑模块组织及数据同步

开发笔记:游戏逻辑模块组织及数据同步
  6 g/ I. s1 q" r9 X2 w7 Y
1 i6 x( w  p9 V& V1 z8 f

- c! K& Z' u- }BagView界面产生一个点击,因为BagController是BagView的观察者,所以BagController能得到点击事件的通知。
+ s' m# I+ ]- [& d: M
2 k1 V2 l" r0 w9 R1 i. @2 `BagController识别出此点击是要出售物品,于是构建好消息包发往服务器。2 t1 I) O( W$ P, ^& K* l' l
- N, a0 d( A9 Y8 H' q& S( l3 g
服务器识别出消息类型是Sell,于是消息被派发给SellHandler。
% D% A  p7 r' c) h& B$ U* e0 S, @+ ^& _$ D; S5 V, V+ v( g' s
SellHandler调用BagController执行逻辑。% B% S4 S4 m, L7 U9 @7 K
* x: k* a3 H8 Y3 h1 n5 ?1 F
BagController取出BagModel和MoneyModel的数据进行条件检查,如果无法执行操作则生成错误码返回给SellHandler,否则调用Model修改数据,此时BagModel会记录下变化物品的id,MoneyModel会做一个脏标记。
9 p2 p  L# q4 B3 J) c9 h4 H
" \! e4 c6 d2 z, A6 uMoneyModel数据发生变化,通知自己的观察者(TaskController)。  H( L2 T+ l0 L$ b

' f2 U( c- h* p. p5 s% ]. @TaskController判断任务完成,调用TaskModel更新数据。TaskModel会记录发生变化的任务。
6 C- b1 o( c6 ]
/ X/ c- S4 K* ^1 H) d; [SellHandler对BagController的调用返回后,如果出错则直接返回消息包给客户端。否则调用SyncController收集同步数据。! ^2 [" _9 E$ `) F) `2 }
7 h8 S$ [+ M; {! G& U" y
SyncController调用各个模块收集同步数据,各个模块提交同步数据后清除自己维护的标记。8 R/ q! w5 Z6 j) i

0 o, P' x# w- |  bSellHandler将操作结果和同步数据打包后发往客户端。
# g) b. s# r4 {' ^- i2 j! ~2 |1 z9 i+ o) [- a
客户端识别出消息类型是Sell,消息被派发给SellHandler。
. Q4 K9 _; j  B3 V8 l0 s, t5 X7 n/ g! K. h) g
BagHandler将消息处理结果发给BagController。
" f& S8 ~( M8 j" Y3 [* e. m- F
5 f6 x. M1 M) B% `- I+ k& R. {BagController根据消息处理结果,通知BagView进行必要的提示。8 i- k' j+ ^- Q; L
3 c, q% Q4 f% G; p) l2 B
SellHandler将消息包中的数据同步部分发给SyncController。0 C; z# v+ i" c  S2 d+ A
0 g0 J0 {: |  w/ M$ P# C
SyncController将同步数据同步给各个模块。
2 U5 A% M5 n; q0 q& |! u3 F* P5 ^
, y) L5 ~# O( \/ iBagModel和MoneyModel的数据发生了变化,通知观察者,即对应的Controller。) q6 x, _! n6 K  V6 p

# ~, D# }; {6 GController调用View进行界面更新。
  Q! I/ h3 e) E% q/ ^; b
  [3 e& K* }+ v( M$ W+ k2 ~0 ?+ zQ&A! m. F) d  Q+ @5 B: ]5 c
0 b2 |0 a" Z9 N4 t3 E* [
返回客户端提交的postback对于网络传输来说太过重量级, 可以尝试改为客户端保存一个rid-postback的键值对, id由客户端自增, 请求数据时把rid一起发送给服务器。( i( b6 q+ R" E; s1 V3 G
, {' ^, G9 R6 d) r0 A4 W" q! m( m
支持这个方案。. K9 e( `! F8 F0 S0 Z4 g
0 j( A: h! h9 U# |  M
但我的想法不是出于数据量的考虑,因为一般网游客户端发往服务器的消息都是比较小的,服务器返回的消息会比较大。 原因是后来我们考虑到消息可能丢包的问题,当丢包发生时,客户端需要重发请求,这样一来rid检验及保存之前发送的请求就是必须的了。而保存下来的请求正好又可以用来替代上文的postback,所以你的方案非常合理。( P: C1 w! @. @% }8 z7 q5 d
$ G# T/ d5 N( L, z) Y0 V
我使用了背包里一个物品,在返回的sync中是返回使用掉的物品信息, 还是背包的全部物品信息?
1 z# p8 U. q) n, C$ W( K; Q! B0 o$ _7 R
因为我们背包里的物品会比较多,所以同步全部物品是不合适的。; Q! k" V. H2 y

0 O# ?3 a4 F8 M/ ^& Y我们的做法是删除物品后记录物品id,生成同步数据时如果发现对应id的物品不存在,则同步一个数量为0的物品信息,客户端收到数量为0的物品后做删除操作。 有的模块没有一个代表删除的特殊“零值”,比如任务。我们的做法是将新增/更新与删除分开同步:7 b$ G4 \% n7 h& P  h8 M# ^
! H8 h% G9 q" W: }3 R. l/ n- S1 H
  • message TaskSync {
  • repeated Task update = 1;
  • repeated int32 delete = 2;
    / j- k7 W: i: ^! Q/ X( h' [' W* u" g}
    - e$ [& U: E- m* k* q) n
转自游资网 http://www.gameres.com/772716.html
: x0 S$ r5 l& O  p- I( F8 c5 \" u& ]0 b
+1
1469°C
1
  • 云小菜
过: 他们
因分享而快乐,学习以自强!
云小菜 发表于 2017-8-31 21:01:08 显示全部楼层
还看不是很明白,收藏先
因分享而快乐,学习以自强!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

VR/AR版块|Unity3d|Unreal4|新手报道|小黑屋|站点地图|沪ICP备14023207号-9|【泰斗社区】-专注互联网游戏和应用的开发者平台 ( 沪ICP备14023207号-9 )|网站地图

© 2001-2013 Comsenz Inc.  Powered by Discuz! X3.4

1
QQ