网站公告 | 泰课在线2017年双十一活动11.7~11.17全站6.8折,三大活动点击查看
查看: 524|回复: 1
收起左侧

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

[复制链接]

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

泰课_robin 发表于 2017-8-31 19:52:07 [显示全部楼层] |只看大图 回帖奖励 |倒序浏览 |阅读模式 回复:  1 浏览:  524
文/++阿联酋长9 U  `# v' w' S/ U/ g8 m$ Y
" q0 p; V3 z) b& B* }/ M
一个游戏根据功能可以划分为多个不同的模块,如金钱、背包、装备、技能、任务、成就等。按照软件工程的思想,我们希望分而治之单独实现不同的模块,再将这些模块组合在一起成为一份完整的游戏。但现实是残酷的,不同模块之间往往有千丝万缕的联系,比如购买背包物品会需要扣金币、打一个副本会完成任务,完成任务又会奖励金币和物品,金币的增加又导致一个成就达成。于是我们虽然在不同的类或不同的文件中来实现各个模块,却免不了模块间的交叉引用和互相调用,最后混杂不堪,任何一点小修改都可以导致牵一发而动全身。
+ y1 w' Z6 H( O: S) _: N4 H
+ r: J: Y. ^# f为了后面说明方便,我们考虑这样一个小型游戏系统:总共有3个模块,分别是金钱、背包、任务。购买背包物品需要消耗金币,卖出背包物品可得到金币,金币增加到一定数额后会导致某个任务的状态变为完成,完成任务可获得物品和金币。这3个模块的调用关系如图。2 ?" u/ g7 L- {
5 E2 j  _( ]# B

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

开发笔记:游戏逻辑模块组织及数据同步
  
( x# D3 W- A: x# O; f% }7 J% P5 G' k3 B, [

) S" f: s3 ^% u8 F0 }  T

7 X) X) Y; K5 o( Y  ]  }首先我们把模块的数据和逻辑分离,借鉴经典的MVC模式,数据部分叫作Model,逻辑部分叫作Controller。如此一来,游戏功能部分就被划分出来了两个不同的层次,Controller处于较高的层次上,可以引用一个或者多个Model。Model层专心处理数据,对上层无感知。每个Model都是完全独立的模块,不引用任何Controller或Model,不依赖于其他任何对象,可以单拿出来进行单元测试。
: ~5 y/ Q% g1 ?: D4 g' t( F" [/ G# R5 e7 u! B

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

开发笔记:游戏逻辑模块组织及数据同步
  
  I7 j- ]/ r0 z) ~! j
: w* ?7 c+ d. D$ j
. V) f6 m+ c) E0 V+ p- `. ^
对于我们的例子,每个模块提供的接口列举如下:" y0 F3 E. B, N' ?& A+ s2 J( E

6 z0 \6 t% ]5 j! y* P. sBagModel:获取物品数量,增加物品,扣除物品. }! w$ @+ D; h3 z! x
8 L. ^9 E+ _1 Q/ u
MoneyModel:获取金币数量,增加金币,扣除金币
! G- Z3 L, p, f9 R8 b( u
" j3 f" d! A: d- y5 u2 z1 \" I6 pTaskModel:增加任务,删除任务,标记任务为完成
& ]# S8 G% z: z) g0 s
( w* z) o$ D. O! j' |BagController:购买物品,卖出物品: S- c1 ^. L# b: R

8 U: F( Y# T; {8 a9 K7 MTaskController:完成任务
" B1 q: F5 V9 I: k( I) I  Q0 t& M
5 D3 a7 f3 k$ g8 E+ {" i' G购买或卖出物品时,由BagController进行或操作校验,随后调用BagModel和MoneyModel完成数据修改。完成任务时,由TaskController调用各个模块。6 s" [. Z! m$ U5 |$ v
- F; l1 L* W6 O/ k- {) ^' }* w
现在唯一的问题是,既然MoneyModel不引用其他模块,那么在金币增加时如何告知任务模块去完成任务呢?这里我们需要引入一个管理依赖的利器:观察者模式。
5 A6 O" Q  n& H; ~6 c& [* d' w) b% p  W4 R: U1 t
具体使用方式是把Model实现为一个Subject,对某个Model的数据变化感兴趣的Controller实现为对应的Observer。我们的例子中,MoneyModel是Subject,在金币数量变化时通知所有已注册的Observer;TaskController是MoneyModel的一个Observer,在初始化时向MoneyModel注册。4 U* ]# z" T( y* O: U4 R* [1 R. }6 a

1 a. v/ c* j2 m

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

开发笔记:游戏逻辑模块组织及数据同步
  # m# Z+ G3 v! a3 x% X7 w

( i1 I& }% ]. @* e& S; N* |/ r! ~. X  K3 q+ L
# S' I4 g* b) g, S9 S
注意图中由MoneyModel指向TaskController的虚线箭头,代表MoneyModel数据变化时会去通知TaskController,用虚线是因为MoneyModel并不依赖于TaskController(只依赖于Observer接口)。同样BagModel也可以提供背包物品变化的Subject,如果新加一个任务是要求某物品的数量达某个值,那么TaskController可向BagModel注册,这样在物品变化时就能得到通知了,图中也画出了这条虚线。
& x3 H$ o0 s+ |% r7 l: B# o% \: d  O
对观察者模式不熟悉的读者朋友可以自行查阅资料, 本文的重点并不是介绍设计模式。这里简单提示一下观察者模式的精髓:当某模块调用其他模块时就产生了依赖,这时可以不直接去调用,而是转而实现一个机制,这个机制就是让其他模块告诉自己他们需要被调用。最后调用的流程没变,变化的是依赖关系。7 e, W9 k5 Z7 [! a/ v: ~0 s% u/ S

* }% k( b# c7 R  K在客户端情况要更复杂一些,实际上加入UI后,我们的模块设计就成经典的MVC,这也是我们为什么把数据模块和逻辑模块分别叫Model和Controller的原因。
2 D2 \. ^6 ]. B9 C/ w) Y# O9 i) w

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

开发笔记:游戏逻辑模块组织及数据同步
  5 a8 C. A$ s: G6 B; a' ^% n( _3 `6 I- K

' p; D0 T, z: |3 j5 k  v) b3 B: z

4 c5 v+ Z  O( @8 O# T9 t这里只画出了背包模块。这里的System API指与游戏运行平台相关的一些接口,可能是操作系统API、引擎API、图形库API等等。View模块和Model模块地位相当,只处理显示而不管游戏功能,需要显示的数据都是由Controller提供的。对于能输入的View同样采用观察者模式,点击等事件发生时通知其他模块(而不是直接调用),注意图中由BagView指向BagController的虚线箭头。: }9 Q1 v% i: d. G0 z9 p

; V; A  s5 k) j9 u$ ~! t3 M下面介绍数据同步的设计。
" f9 p! X$ b  b: a7 N+ F$ I0 J9 m8 z4 T
首先对于网络游戏,客户端所展示的数据是服务器传送过来的。当玩家操作导致数据发生变化时,最好也由服务器更新给客户端。曾经接手过一个项目,很多操作的结果都是客户端先算出来的,于是各种逻辑都是服务器和客户端各实现一遍,很容易两边的数据就不一致了,很让人头疼。
+ v& b! a- i- N6 `! k" O
: S6 s0 P) @+ e+ O6 b( V2 Q* O- @所以我们的同步思路是当客户端向服务器发起一个请求时,服务器将所有变化的数据同步给客户端,客户端收到服务器的返回后再更新数据,绝不私自改动数据。在这个指导思想下,我们消息包结构是这样的(以物品卖出举例):
9 e& @- r. V- {" b5 ^  }: K2 J" I$ A# b: ?, F6 p
  • message BagItemSellCG {
  • optional int32 id = 1;
  • opitnoal int32 count = 2;
  • }
  • message BagItemSellGC {
  • optional int32 result = 1;
  • optional Sync sync = 2;
  • opitonal BagItemSellCG postback = 3;  e; K4 y' C) w/ S8 I- G
    }
    1 y- @& C$ P4 \; n+ F& e7 R

2 \+ E5 A8 A5 @3 U& Z4 O& I9 @; x复制代码
0 e( r2 h- `9 U$ I4 O! z
. _. R% @; J+ u5 J6 b8 t服务器向客户端返回的消息几乎总是包含3个字段。result为操作结果可能是0或者错误码,sync中包含了所有的数据更新,postback将客户端的请求消息原封不动返回去,便于客户端进行界面更新或友好提示。* U4 x* {: j# j

0 r6 [* K  y8 {# I& ~6 Hsync是一个比较复杂的message,包含了所有需要更新的Model的数据。感谢Protocol Buffer的optional选项,大多数情况下我们发送的数据只是其中很小的一部分。
" W5 B6 S' g0 h3 E
$ u3 e* A0 ^2 I4 D先来看服务器端消息处理和同步的设计。
3 @" u& ~9 _" A9 m% v' E+ Q& c$ b$ i1 N% A2 p7 K

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

开发笔记:游戏逻辑模块组织及数据同步
  * q4 v0 b3 ?" G1 {& D2 u
1 t3 i9 M" p# ]0 X

9 L& ^7 B4 n9 z0 Z# N. R如图所示,我们在Model和Controller之上新加了一个Handler接口层。Handler负责解析消息包,调用Controller处理消息包,在必要的时候调用SyncController构建同步数据,最后打包成消息返回给客户端。' F- S2 d& ]0 ^; l& B  B
0 _6 J+ A& O( n9 C) V4 A3 l
每个Model在管理数据的基础上会维护变化数据的集合,对于简单的Model比如MoneyModel就是一个bool脏标记,而BagModel则维护变化物品id的集合。变化数据列表在同步之后清除。
/ Y! _7 Y9 N0 [+ n
+ J+ o+ |# s+ O* H) R# ]7 i7 t* B客户端的结构是类似的。
$ t3 w6 K+ y4 Z: M/ j
7 R1 u- U* J0 I5 \

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

开发笔记:游戏逻辑模块组织及数据同步
  + I* g6 E" M7 e, @

- U$ ]) L6 L3 P" q与服务器的区别就在于SyncController是负责调用Model更新数据,每个Model都实现数据更新接口。注意除SyncController之外,其他Controller只能读取Model而不能改变其数据,这样就保证了所有数据一定是从服务器同步的。" Z/ u! o& K* x

8 }% x+ j9 Y" O! I3 b最后我想以出售物品为例子完整走一遍流程。从客户端进行操作开始,到请求发到服务器,最后再返回客户端更新数据和界面。完整的图比较复杂,混在一起基本上没法看了,只好删掉了客户端的任务模块……- X9 h: m. f. c9 c, K

  Z" K' M. o" u- ?- D" w1 p" I$ Y5 Y

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

开发笔记:游戏逻辑模块组织及数据同步
  9 Z# P# D; a" j2 z7 P
4 |# u8 }4 f! Z- g; t* r. ~6 I/ R
; v6 a$ o, i; D2 g6 \
BagView界面产生一个点击,因为BagController是BagView的观察者,所以BagController能得到点击事件的通知。9 ~" G# `4 j) v. g8 N/ l: i

& Q, R, s/ l; t% G& \$ j6 KBagController识别出此点击是要出售物品,于是构建好消息包发往服务器。8 e4 I7 L4 w4 p. y; z
6 b! s6 m, O0 I# T
服务器识别出消息类型是Sell,于是消息被派发给SellHandler。1 d. G% ?' M& S! @; y$ a
6 W" g" l5 j) P* E! H# M0 l
SellHandler调用BagController执行逻辑。5 }8 B" e9 K2 h0 q! i

% H% G1 \* J1 u# ?6 J6 ZBagController取出BagModel和MoneyModel的数据进行条件检查,如果无法执行操作则生成错误码返回给SellHandler,否则调用Model修改数据,此时BagModel会记录下变化物品的id,MoneyModel会做一个脏标记。# e  o: R+ h5 K5 _5 n2 c; ~

$ {6 T3 n+ X( y5 B7 j2 j' k" KMoneyModel数据发生变化,通知自己的观察者(TaskController)。
& W: o# i5 K) H; ^
: J0 I9 K8 ^  r, {  x. tTaskController判断任务完成,调用TaskModel更新数据。TaskModel会记录发生变化的任务。
' e7 R$ B! z2 x8 j4 R6 N2 J. |1 c, ]( Q- C1 F2 S, u
SellHandler对BagController的调用返回后,如果出错则直接返回消息包给客户端。否则调用SyncController收集同步数据。; |% Y1 O# a! R1 v5 W9 e  s- N
7 L8 E0 e) U3 N. k* {
SyncController调用各个模块收集同步数据,各个模块提交同步数据后清除自己维护的标记。# \  ~  f* e9 N9 K$ I8 N# W

; {' }; O( m, W% ~! t5 h. YSellHandler将操作结果和同步数据打包后发往客户端。4 u! @3 ]$ @5 N; Z
& V1 d& X1 V: j
客户端识别出消息类型是Sell,消息被派发给SellHandler。! }- g2 Q- Y% Y  t; z& {8 h
9 k' ^2 n  e3 r2 \
BagHandler将消息处理结果发给BagController。5 T) n! k# x$ T+ F, @, L
& n4 [+ m7 k, H$ d( w* b
BagController根据消息处理结果,通知BagView进行必要的提示。; s9 z/ ?5 e% Q" O

6 l: l1 d% i: I* b6 x3 ?# cSellHandler将消息包中的数据同步部分发给SyncController。
6 i+ p, x- C8 H$ Y7 I: V! g1 \1 E; y' T
SyncController将同步数据同步给各个模块。" U- x: I# L/ ~6 q/ [; v& V- W
; P7 C2 \7 `# \2 w7 O# p7 X+ K
BagModel和MoneyModel的数据发生了变化,通知观察者,即对应的Controller。
9 u" G! K5 Z9 n2 S0 z/ Q
- U4 Z6 v/ T8 a0 Q9 BController调用View进行界面更新。
- T. H$ R* @+ C5 B5 B! Y; J- r, T$ i% V) n+ _* s. n1 h( r
Q&A
1 h5 m) L$ t& T. ~3 @& e0 y. T' }& w5 b/ U
返回客户端提交的postback对于网络传输来说太过重量级, 可以尝试改为客户端保存一个rid-postback的键值对, id由客户端自增, 请求数据时把rid一起发送给服务器。
( F7 d8 ~. l. e8 L  v1 s
5 G7 w! Z  s, w9 M支持这个方案。3 }7 O9 P8 V- o( W8 Y2 v
6 @3 t( s2 n' A/ z" \) [6 V
但我的想法不是出于数据量的考虑,因为一般网游客户端发往服务器的消息都是比较小的,服务器返回的消息会比较大。 原因是后来我们考虑到消息可能丢包的问题,当丢包发生时,客户端需要重发请求,这样一来rid检验及保存之前发送的请求就是必须的了。而保存下来的请求正好又可以用来替代上文的postback,所以你的方案非常合理。
8 E; n6 M, H& H9 p
$ h$ W* |) Z7 t8 j我使用了背包里一个物品,在返回的sync中是返回使用掉的物品信息, 还是背包的全部物品信息?( }. X1 O1 V& f. r/ F3 w5 |- U

9 h1 Q. |) R  J  p, B: U因为我们背包里的物品会比较多,所以同步全部物品是不合适的。0 @  s# _: }7 J2 p& g$ q& V5 ^# h4 w

1 t! Y7 f( \6 [7 X/ a# c, Z8 [我们的做法是删除物品后记录物品id,生成同步数据时如果发现对应id的物品不存在,则同步一个数量为0的物品信息,客户端收到数量为0的物品后做删除操作。 有的模块没有一个代表删除的特殊“零值”,比如任务。我们的做法是将新增/更新与删除分开同步:! ^2 S) ~2 l  S0 O

& x7 ?$ a5 l- t0 O  q; s5 P5 x7 m
  • message TaskSync {
  • repeated Task update = 1;
  • repeated int32 delete = 2;
    6 o' W5 z( u- ?: Y1 y}
    , |+ y3 U5 Z& m9 o0 l
转自游资网 http://www.gameres.com/772716.html
6 J0 _2 O' e: ~1 Y' I
+1
516°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.2

1
QQ