锁定老帖子 主题:网络游戏开发中的领域模型
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2008-07-31
一直以来我都在思考网络游戏开发的OO模式。如何将领域建模运用到网络游戏这种异步项目中。在我的网络游戏架构中,为了避免多线程引发的诸多问题,逻辑服务模块采用单线程方式,考虑到单线程下阻塞模式会严重影响服务器的性能,因此采用了异步方式进行逻辑的通讯。我想大多的网络游戏开发应该都是采用异步方式。但异步方式,很容易将一个原本完整的逻辑,折分成一个一个request、response等网络命令,服务器端将一些逻辑代码写在收到指令的地方,另一些写在业务类中,同样在收到指令的地方调用。没有一种清晰的思路来区分哪些逻辑该和异步的收发混在一起,哪些逻辑该放在领域层。
――――――――――应用层―――――――――――― // 客户端的购买道具逻辑代理 class ShoppingAgent : public ServiceAgent { public: // 发出购买某道具的 void requestBuyItem(int itemId, int qty) { BuyItemRequest request(itemId, qty); send(request); } void onBuyItemResponse(BuyItemResponse response) { if(response.errorId == NO_ERROR) m_listener->onBuyItemOk(); else m_listener->onBuyItemFail(); } protected: ShoppingListener m_listener; } // 服务端购买道具的逻辑服务 class ShoppingService : public Service { public: // 响应购买道具的请求 void onBuyItemRequest(BuyItemRequest r) { BuyItemResponse response; Shopping shopping; try { shopping.buyItem(r.playerId, r.shopId, r.itemId, r.qty); } catch(ShoppingException e) { // 购买失败的错误ID会跟随response发回给客户端 response.errorId = e.errorId; } send(response); } }
应用层,在这里主要负责对网络的异步通讯封装。ShoppingService作为服务在部署在服务器上,而ShoppingAgent作为访问服务的一个接口部署在客户端。对于客户端的应用上层,不需要调用到任何通讯指令,全部委托给ShoppingAgent。异步的结果,皆通过观察者模式,通知给上层。
――――――――――领域层―――――――――――― // 购买道具的服务 //(对应Erice Evans 在Domain 中的service) class Shopping { public: // 购买道具 void buyItem(int playerId, int shopId, int itemId, int qty) { Shop shop = m_respository.getShop(shopId); Player player = m_respository.getPlayer(playerId); // 验证商店是否有要购买的道具 validateShopHasItem(shopId, itemId, qty); // 取出道具价格 int amount = shop.getPrice(itemId, qty); // 验证玩家是否有足够的钱 validatePlayerHasMoney(playerId, amount); // 验证玩家背包是否有足够空间 validatePlayerHasSpace(playerId, itemId, qty); // 玩家收到一定数量的道具 player.receiveItem(itemId, qty); // 玩家付款 player.pay(amount); } protected: void validateShopHasItem(shopId, itemId, qty) { Shop &shop = m_respository.getShop(shopId); if(shop.hasItem(itemId, qty)) throw new ShopHasNoItemException(); } void validatePlayerHasSpace(int playerId, int itemId, int qty) { Player &player = m_respository.getPlayer(player); Item &item = m_itemRespository.getItem(itemId); if(player.getBag().getSpaceAvailabe() < item.getCapability() * qty) { throw new NoEnoughSpaceException(); } } void validatePlayerHasMoney(int playerId, int price) { Player &player = m_respository.getPlayer(player); if(player.hasMoney(price)) { throw new NoEnoughMoneyException(); } } } // 商店,提供道具的购买,对道具定价(不同商店卖的东西不同) //(对应Erice Evans 在Domain 中的entity) class Shop { public: int getPrice(int itemId, int qty) { // …查找商品价格并乘上数量 } bool hasItem(int itemId, int qty) { // …查找商品 } } // 玩家 //(对应Erice Evans 在Domain 中的entity) class Player { public: void receiveItem(int itemId, int qty) { getBag()->pack(itemId, qty); } void pay(int money) { m_money -= money; } } // 资源仓库,由于是例子,所以先写到一块来 class Respoistory { public: Shop& getShop(int id) { } Player& getPlayer(int id) { } Item& getItem(int id) { } } Shopping 同Shop的分离,是我重新看DDD文章后的重大改变。不然之前一直困惑于buyItem()中会对于玩家的一些逻辑操作,似乎不应该是shop这个领域Entity的职责。
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2008-08-01
我们来做一个重构先:
validatePlayerHasMoney(playerId, amount); validatePlayerHasSpace(playerId, itemId, qty); 这个职责完全是可以移到player上的,player知道自己又多少钱,自己背包又多大,也可以把东西放到自己背包里去。如果是用intellij,把这个方法标记为static,然后按refactor->replace with instance method就可以了。 领域模型和存储与通讯是无关的。又两种做法,一种是把东西都load内存中,然后再内存中操作领域对象,然后该持久的持久,该传输的传输。另外一种做法是proxy pattern。一个最实际的例子是hibernate的lazy load,表面上你是访问一个list,实际上这个时候触发了一条SQL。 推荐你先尝试第一种方案,以一个transaction为单位,在开始的时候把数据load出来,再结束的时候把数据存回去。如果中途需要按需load或者save数据,则要考虑采用更复杂的proxy模式。 不过最紧要的是,为什么DDD?你的目的是什么? |
|
返回顶楼 | |
发表时间:2008-08-01
assume you run everything in the same process, then do you ddd. So you ignore the network complexity first to see whether you can come up something.
A good design should be able to sustain the changes when you add in the network factor. Buyer { private CashAccount account; private ItemBag bag; buy(Item item, Shop shop) { if (hasSpace()) { double price = shop.getPrice(item); // throw ItemNotFound exception if not found, or return a negative number and then check that. if (hasEnoughCash()) { shop.exchange(this.account, this.bag, item); } else throw exception("no enough cash!"); } else throw exception("no enough space"); } } When you expand this to a client server model, you have the Shop on one side, and the buyer is on the other. In this case, the Shop will be acquired remotely. So shop will have a different implementation than the default one. So use an interface for Shop and has two implementations, the remote one wrap the network code around the default one and delegate all business operations to it. So the core business should be able to run in one process so that we could test it easily and fast. When we add a behavior to the core, we really don't care the network. A good design serves as the skeleton where we stick muscles to it. |
|
返回顶楼 | |
发表时间:2008-08-01
建议不要自己去闭门造车
MMOG的开发和企业开发思维是不一样的 推荐sun的 project darkstar(http://projectdarkstar.com/)的开源项目,这个是sun的大型网游的服务端参考框架 一般来说,网游用java开发服务端的不多,用c++和python的比较多 python的高性能网络编程框架twisted比较合适,可以参考一下 有空多交流 |
|
返回顶楼 | |
发表时间:2008-08-01
taowen 写道 我们来做一个重构先:
validatePlayerHasMoney(playerId, amount); validatePlayerHasSpace(playerId, itemId, qty); 这个职责完全是可以移到player上的,player知道自己又多少钱,自己背包又多大,也可以把东西放到自己背包里去。如果是用intellij,把这个方法标记为static,然后按refactor->replace with instance method就可以了。 感谢你的重构建议,的确这段代码写得有点问题,player用得不够好,我现在改成了player.hasSpace()和player.hasCash()两支函数。但我还是比较坚持单独两支validate函数,因为player.hasSpace()和player.hasCash()都只是以bool返回,validate加上了异常的抛出,而player不需要负责异常抛出。 |
|
返回顶楼 | |
发表时间:2008-08-01
taowen 写道 领域模型和存储与通讯是无关的。又两种做法,一种是把东西都load内存中,然后再内存中操作领域对象,然后该持久的持久,该传输的传输。另外一种做法是proxy pattern。一个最实际的例子是hibernate的lazy load,表面上你是访问一个list,实际上这个时候触发了一条SQL。 推荐你先尝试第一种方案,以一个transaction为单位,在开始的时候把数据load出来,再结束的时候把数据存回去。如果中途需要按需load或者save数据,则要考虑采用更复杂的proxy模式。 我这里主要是遇上了必须用异步来提供服务的问题,数据库的加载,一般是同步机制。 taowen 写道 不过最紧要的是,为什么DDD?你的目的是什么?
之所以后DDD,首先是认同DDD的主体思想,觉得应该让游戏逻辑开发人员更多的精力关注在领域模型上,所以决定将网络服务的异步收发同领域两层分离,找到合适的一种开发模式。当然,主要是关心domain model的概念,倒还不注重于Driven Desgin |
|
返回顶楼 | |
发表时间:2008-08-01
JeffreyHsu 写道 建议不要自己去闭门造车
MMOG的开发和企业开发思维是不一样的 同意MMOG和企业开发有不一样的地方,但由于MMOG国内在谈架构的人相对较少,而企业开发的人谈这些概念的较多,所以就在这里发贴。另外我觉得企业开发的很多东西和MMOG也是相通的。难道MMOG就不需要领域建模的思想了?我想领域建模主要是希望大家把关注点从对环境关注(如web开发)拉到领域层面。游戏里有大量复杂的领域模型,甚至往往比企业开发的模型还多样化,所以我个人觉得领域建模主体思想不只适用于企业开发。 JeffreyHsu 写道 一般来说,网游用java开发服务端的不多,用c++和python的比较多 python的高性能网络编程框架twisted比较合适,可以参考一下 另外你可能误会了,其实我也是用c++开发游戏。只是c++圈内谈这些概念的人少,只好混到java这边来了。 JeffreyHsu 写道 推荐sun的 project darkstar(http://projectdarkstar.com/)的开源项目,这个是sun的大型网游的服务端参考框架 感谢你推荐的网址,我会去好好学习,如果你也是开发游戏的,我也很希望向你多学学游戏的架构。 |
|
返回顶楼 | |
发表时间:2008-08-01
jellyfish 写道 Buyer { private CashAccount account; private ItemBag bag; buy(Item item, Shop shop) { if (hasSpace()) { double price = shop.getPrice(item); // throw ItemNotFound exception if not found, or return a negative number and then check that. if (hasEnoughCash()) { shop.exchange(this.account, this.bag, item); } else throw exception("no enough cash!"); } else throw exception("no enough space"); } } 我原本将Shopping类作为一个领域层的服务类,提供了购买的服务。当时是考虑到如果player.buy()不应该知道shop,如果shop.buy()不应该知道player。所以才引入一个Shopping类作为服务。这里你用了一个buyer,请问buyyer这里是一个service,还是一个entity?如果是entity的话,那player和buyer之间是否是继承关系? jellyfish 写道 When you expand this to a client server model, you have the Shop on one side, and the buyer is on the other. In this case, the Shop will be acquired remotely. So shop will have a different implementation than the default one. So use an interface for Shop and has two implementations, the remote one wrap the network code around the default one and delegate all business operations to it. 我一开始也是这种想法,通过一个interface,然后两个implementation,一个负责远程访问封装,一个纯粹用于逻辑。可是由于我的网络是异步访问的,所以很难让一个接口来体现。我也曾问过我们项目中网络开发负责人,如果用成同步方式,可以吗?当时我就是为了能搞成这种方式。结果被驳了一番,焦点是用了同步,就得用多线程,不然别的处理就不要做了,但多线程引入带来的一系列问题,会令项目后期遇上很多麻烦。至少多线程的BUG你就很难debug了。 jellyfish 写道 So the core business should be able to run in one process so that we could test it easily and fast. When we add a behavior to the core, we really don't care the network. 呵呵,我想要独立领域层的一个大目的,就是为了好unit test。 |
|
返回顶楼 | |
发表时间:2008-08-03
The shop object is passed in the buy() method, so the buyer doesn't know the shop in its internal state. You have to pass this object in order to know to buy from where.
Treat buyer as player in your context. Sorry, I didn't follow your context. The shop interface has nothing to do with the implementation, where you across the network or not, or you use sync or async way or not. Under the implementation package, you may have various implementations to hook in. In several of my projects, I had one interface with implementations, single threaded, multi-threaded, jms-sync, jms-asyn, grid1-sync, grid1-async, grid2-sync, grid2-async. all non-single-threaded versions will eventually delegate the business logic to the single-threaded version, after handling its own concerns. Users can choose which through an input parameter during runtime. Basically, this is just a usage of decorator pattern. You can implement an interface using async methods. The key is to manager your data from the start to the end. In my implementations, I always has subpackages request and reply to distinguish the methods, this is extremely helpful for new developers so they wont get lost in the jungles. My key point is async or sync is not a business concern, and thus should be abstract away from the business interface. Net game players don't care that. I was one of the net players, I hated to see system errors, :-). My proposed way is to code the business model in a single threaded env first. Then expand it to other async, distributed env. This is a very effective way in my experience and is easier to convince others because you can see the results right away. Good luck. Been there, done that, not easy. Hope get paid well, :-). |
|
返回顶楼 | |
发表时间:2008-08-04
我也是希望同步异步,单线多线脱离于领域逻辑的编写。但一个interface多种implementation的具体细节,我还是不是很通。就拿buyer和shop来说吧,buyer.buy(Item item, Shop shop)
{ // ... shop.exchange(this.account, this.bag, item); // ... } 在exchange处理完后,buy逻辑才会继续。如果这时候shop在另一台机上,就必须等待。但我们又不希望这样的等待影响到别的客户端的请求。又不愿用多线程来处理请求,结果就只能折分buy的逻辑成为几个请求响应步骤了。于是还是没能解决困惑。 jellyfish 写道 In several of my projects, I had one interface with implementations, single threaded, multi-threaded, jms-sync, jms-asyn, grid1-sync, grid1-async, grid2-sync, grid2-async. all non-single-threaded versions ....
请问什么是jms-sync,jms-asyn,grid1-sync,grid1-async...,我怎么在goolge和baidu中都搜索不到? jellyfish 写道 Basically, this is just a usage of decorator pattern.
还是不明白decorator怎么解决,是否可以简单举个例子 |
|
返回顶楼 | |
浏览 6056 次