炉石传说 C# 设计文档(序)
经过3个月的开发,有很多感触。
以前一直以为技术是开发成败的第一因素,现在发现,等到你代码写的时间够长,经验够丰富,什么功能都能随手完成,对于业务的分析能力变成了第一位。
炉石山寨版的BS版本用到的HTML5的SVG,我看了一个下午的教程,借鉴以前GUI+和HTML的经验,很快就能写点东西出来了。
WebSocket,Github上找了一个开源的C#项目,通讯这块也是几个小时就搞定了。Javascript不是很熟悉,当时闭包这样的一些概念也算听说过,Js也是无障碍就写成了。
整个项目的技术壁垒其实不是很高,难的是对于炉石的业务的理解。
设计一个项目,就是将项目分割成若干个子系统,然后用适当的设计模式,让代码出现在它该出现的地方。
一个项目的代码量,肯定是不断增加,然后通过重构减少,然后又加入新的子系统,导致代码量的增加,再通过重构减少,这样的螺旋形的折腾和反复(迭代)使得代码越来越完善。
同时,随着对于业务的理解,很有可能会对原来的设计产生颠覆性的修改。炉石的开发中,我3次做了颠覆性的设计的修改。
血的教训告诉我们,开发之前,一定要做好业务的研究。
如果你觉得你的代码这样写也不好,那样写也不对,不用纠结了,好好睡一觉,然后重新整理一下业务,修改一下你的业务模型,然后所有问题就迎刃而解了。
中国的外包公司,包括一些有名的大公司,都不喜欢写文档,或者文档落后与代码。
代码可以上线,可以取得业绩,文档似乎完全只是为了应付CMM的规定,所以,别说考虑文档的美观了,就是一个和代码同步的文档,对于大多数公司来说,也很奢侈。
很多人有一个观点,好的代码是不需要注释和文档的,代码就是最好的注释和文档。
其实,我觉得,文档不是伪代码,应该是对于业务的一种解释,以及编码的一个依据。
我觉得需要这些枚举,是通过怎样的调查分析得到的结论。
我觉得业务流程是怎样的,我通过分析业务,画出流程图得到的一个结果。
很多人说,炉石的C#代码通过反编译可以看得到,为什么要重复去山寨呢?
这段时间工作很空闲,写代码写得旧了,所以想通过山寨炉石来提高自己的分析和设计能力。
炉石其实你真正的考虑怎么设计的好,怎么使得你的设计可以同时满足BS,CS,网络版,单机版,也是非常不容易的。然后炉石的业务,如果很多地方不考虑扩展性,也不是很复杂,一个月足够完成所有的编码工作了。
但是,如果要考虑到扩展性,考虑到重复使用,考虑到IOC这样的东西,则需要好好考虑的。
例如,施放法术,
我写到最后就是一个施放法术的接口(抽象),然后各种法术的具体实现,
施法流程是调用了施放法术的接口,通过法术数据去调用具体的实现。
法术接口:
(博客园的插入代码推荐有了,旁边那个插入代码没有存在的意义了,个人觉得)
using Engine.Action; using Engine.Card; using Engine.Client; using System; using System.Collections.Generic; namespace Engine.Effect { public interface IAtomicEffect { /// <summary> /// 对方复原操作 /// </summary> /// <param name="game"></param> /// <param name="actField"></param> void ReRunEffect(ActionStatus game, String[] actField); /// <summary> /// 对英雄动作 /// </summary> /// <param name="game"></param> /// <param name="PlayInfo"></param> /// <returns></returns> String DealHero(ActionStatus game, PublicInfo PlayInfo); /// <summary> /// 对随从动作 /// </summary> /// <param name="game"></param> /// <param name="Minion"></param> /// <returns></returns> String DealMinion(ActionStatus game, MinionCard Minion); /// <summary> /// 获得效果信息 /// </summary> /// <param name="InfoArray"></param> void GetField(List<String> InfoArray); } }
一个伤害效果
using Engine.Action; using Engine.Client; using Engine.Control; using Engine.Utility; using System; using System.Collections.Generic; namespace Engine.Effect { /// <summary> /// 攻击效果 /// </summary> public class AttackEffect : IAtomicEffect { /// <summary> /// 效果表达式 /// </summary> public String 伤害效果表达式 = String.Empty; /// <summary> /// 伤害加成 /// </summary> public Boolean 伤害加成 = false; /// <summary> /// 获得效果信息 /// </summary> /// <param name="InfoArray"></param> void IAtomicEffect.GetField(List<string> InfoArray) { 伤害效果表达式 = InfoArray[0]; 伤害加成 = ExpressHandler.GetBooleanExpress(InfoArray[1]); } /// <summary> /// 对英雄动作 /// </summary> /// <param name="game"></param> /// <param name="PlayInfo"></param> /// <returns></returns> String IAtomicEffect.DealHero(ActionStatus game, Client.PublicInfo PlayInfo) { int AttackPoint = ExpressHandler.GetEffectPoint(game, 伤害效果表达式); //调整伤害值 if (伤害加成) AttackPoint += game.AllRole.MyPublicInfo.BattleField.AbilityDamagePlus; if (PlayInfo.Hero.AfterBeAttack(AttackPoint)) { game.battleEvenetHandler.事件池.Add(new Engine.Utility.CardUtility.全局事件() { 触发事件类型 = CardUtility.事件类型枚举.受伤, 触发位置 = PlayInfo.Hero.战场位置 }); } return Server.ActionCode.strAttack + CardUtility.strSplitMark + PlayInfo.Hero.战场位置.ToString() + CardUtility.strSplitMark + AttackPoint.ToString(); } /// <summary> /// 对随从动作 /// </summary> /// <param name="game"></param> /// <param name="Minion"></param> /// <returns></returns> String IAtomicEffect.DealMinion(ActionStatus game, Card.MinionCard Minion) { int AttackPoint = ExpressHandler.GetEffectPoint(game, 伤害效果表达式); //调整伤害值 if (伤害加成) AttackPoint += game.AllRole.MyPublicInfo.BattleField.AbilityDamagePlus; if (Minion.设置被攻击后状态(AttackPoint)) { game.battleEvenetHandler.事件池.Add(new Engine.Utility.CardUtility.全局事件() { 触发事件类型 = CardUtility.事件类型枚举.受伤, 触发位置 = Minion.战场位置 }); } return Server.ActionCode.strAttack + CardUtility.strSplitMark + Minion.战场位置.ToString() + CardUtility.strSplitMark + AttackPoint.ToString(); } /// <summary> /// 对方复原操作 /// </summary> /// <param name="game"></param> /// <param name="actField"></param> void IAtomicEffect.ReRunEffect(ActionStatus game, string[] actField) { int AttackPoint = int.Parse(actField[3]); if (actField[1] == CardUtility.strYou) { //MyInfo if (actField[2] == Client.BattleFieldInfo.HeroPos.ToString("D1")) { game.AllRole.MyPublicInfo.Hero.AfterBeAttack(AttackPoint); } else { game.AllRole.MyPublicInfo.BattleField.BattleMinions[int.Parse(actField[2]) - 1].设置被攻击后状态(AttackPoint); } } else { //YourInfo if (actField[2] == Client.BattleFieldInfo.HeroPos.ToString("D1")) { game.AllRole.MyPublicInfo.Hero.AfterBeAttack(AttackPoint); } else { game.AllRole.MyPublicInfo.BattleField.BattleMinions[int.Parse(actField[2]) - 1].设置被攻击后状态(AttackPoint); } } } } }
/// <summary> /// 实施效果 /// </summary> /// <param name="singleEffect"></param> /// <param name="game"></param> /// <param name="RandomSeed"></param> /// <returns></returns> public static List<string> RunSingleEffect(EffectDefine singleEffect, ActionStatus game, int RandomSeed) { List<string> Result = new List<string>(); List<string> PosList = SelectUtility.GetTargetList(singleEffect.AbliltyPosPicker, game, RandomSeed); foreach (string PosInfo in PosList) { var PosField = PosInfo.Split(CardUtility.strSplitMark.ToCharArray()); var strResult = string.Empty; if (PosField[0] == CardUtility.strMe) { switch (int.Parse(PosField[1])) { case BattleFieldInfo.HeroPos: Result.Add(GetEffectHandler(singleEffect, game, CardUtility.strMe + CardUtility.strSplitMark + BattleFieldInfo.HeroPos.ToString("D1")).DealHero(game, game.AllRole.MyPublicInfo)); break; case BattleFieldInfo.AllMinionPos: for (int i = 0; i < game.AllRole.MyPublicInfo.BattleField.MinionCount; i++) { Result.Add(GetEffectHandler(singleEffect, game, CardUtility.strMe + CardUtility.strSplitMark + (i + 1).ToString("D1")).DealMinion(game, game.AllRole.MyPublicInfo.BattleField.BattleMinions[i])); } break; case BattleFieldInfo.AllRolePos: Result.Add(GetEffectHandler(singleEffect, game, CardUtility.strMe + CardUtility.strSplitMark + BattleFieldInfo.HeroPos.ToString("D1")).DealHero(game, game.AllRole.MyPublicInfo)); for (int i = 0; i < game.AllRole.MyPublicInfo.BattleField.MinionCount; i++) { Result.Add(GetEffectHandler(singleEffect, game, CardUtility.strMe + CardUtility.strSplitMark + (i + 1).ToString("D1")).DealMinion(game, game.AllRole.MyPublicInfo.BattleField.BattleMinions[i])); } break; default: Result.Add(GetEffectHandler(singleEffect, game, PosInfo).DealMinion(game, game.AllRole.MyPublicInfo.BattleField.BattleMinions[int.Parse(PosField[1]) - 1])); break; } } else { switch (int.Parse(PosField[1])) { case BattleFieldInfo.HeroPos: Result.Add(GetEffectHandler(singleEffect, game, CardUtility.strYou + CardUtility.strSplitMark + BattleFieldInfo.HeroPos.ToString("D1")).DealHero(game, game.AllRole.YourPublicInfo)); break; case BattleFieldInfo.AllMinionPos: for (int i = 0; i < game.AllRole.YourPublicInfo.BattleField.MinionCount; i++) { Result.Add(GetEffectHandler(singleEffect, game, CardUtility.strYou + CardUtility.strSplitMark + (i + 1).ToString("D1")).DealMinion(game, game.AllRole.YourPublicInfo.BattleField.BattleMinions[i])); } break; case BattleFieldInfo.AllRolePos: Result.Add(GetEffectHandler(singleEffect, game, CardUtility.strYou + CardUtility.strSplitMark + BattleFieldInfo.HeroPos.ToString("D1")).DealHero(game, game.AllRole.YourPublicInfo)); for (int i = 0; i < game.AllRole.YourPublicInfo.BattleField.MinionCount; i++) { Result.Add(GetEffectHandler(singleEffect, game, CardUtility.strYou + CardUtility.strSplitMark + (i + 1).ToString("D1")).DealMinion(game, game.AllRole.YourPublicInfo.BattleField.BattleMinions[i])); } break; default: Result.Add(GetEffectHandler(singleEffect, game, PosInfo).DealMinion(game, game.AllRole.YourPublicInfo.BattleField.BattleMinions[int.Parse(PosField[1]) - 1])); break; } } } return Result; }
最后,通过对于业务的不断理解,有一些看上去不一样的东西变得一样了。
法术,光环,战吼,亡语,其实都是一样的
一些看上去一样的东西,变得不一样了
奥秘有的是修改触发行为,有的是追加效果;光环有的是影响战场的其他随从,有的是被战场影响
等到你将业务真正分析清楚了,写代码就是一个体力活了。
而且,作为程序员,修改自己的代码,大家都不情愿,辛辛苦苦写的代码不愿意修改。
如果文档先行,修改一下文档,大家还是很乐意的,不用测试,不用返工。而且修改文档是时间成本最小的。如果上线后再修改BUG,那个时间成本。。。。。
题外话:
VS14CTP已经开始使用了,ASPNET的vNext版本也开始尝试了,然后KRE的Self-Host功能也做个试验了,部署在远程服务器上,本地完全可以访问。
但是,性能不能和原生的IIS相比,园子里面有一篇文章介绍过的,地址忘记了,大概相差两三倍吧。
第一次玩MVC(技术面试的时候,这个好像一定要问的,没有玩过MVC好像就不懂ASP一样,一直纯手工写WebForm的人,情何以堪,IOC好像也是必问的,现在的技术面试太看重理论了)
,蛮有趣的,但是我有个疑问,MVC加上EF对于关系型数据库支持的很少,如何让MVC和NOSQL一起工作呢?
NOSQL的元数据对于MVC来说,总觉得会出现不兼容的情况。。。。