Silverlight游戏研究笔记
最近研究深蓝色左手的《C#开发WPF/Silverlight动画及游戏系列教程》
http://www.cnblogs.com/alamiye010/archive/2009/06/17/1505346.html
笔记心得如下:
1.人物移动分成两部分:质心的移动,以及人物自身的移动动画,这不由让我想起了大四最后一门课《刚体力学》。
2.对于Image,在WPF是:
spirit.Source = new BitmapImage(new Uri(@"Player\" + count + ".png", UriKind.Relative));
而在Silverlight则是:
spirit.Source = new BitmapImage(new Uri(@"Player/" + count + ".png", UriKind.Relative));
3.A*算法
终于想明白为什么过去玩游戏,人物总是先走正方形的对角线,然后再走横边,原来是A*算法的一种实现方式。
4.要实现Save/Load,就要使用Memento
而要实现Record,就是实现Command,把需要存储的操作作为Command存储到集合中。
5.副本地图绝对是好东西,但要结合地图编辑器来实现。
6.SL中,使用副本地图。
获取指定图片的某像素点的颜色
private void img3_MouseMove(object sender, MouseEventArgs e)
{
WriteableBitmap bitmap = new WriteableBitmap(img3, null);
int color = bitmap.Pixels[(int)e.GetPosition(img3).Y * (int)img3.ActualWidth + (int)e.GetPosition(img3).X];
// 将整型转换为字节数组
byte[] bytes = BitConverter.GetBytes(color);
// 将字节数组转换为颜色(bytes[3] - A, bytes[2] - R, bytes[1] - G, bytes[0] - B)
lbl.Text = Color.FromArgb(bytes[3], bytes[2], bytes[1], bytes[0]).ToString();
}
7.WPF中的System.Drawing.Point,在SL中则是new System.Windows.Point
8.silverlight技巧 获取鼠标滚轮事件 及 判断获取组合键的方法
9.貌似Resource对应的../ 但是只能是Clip这样的
而Content对应的/Images 用于WB
10.X=Left+CenterX
终于把怪物的坐标轴搞好了。就是把静止物体的公式中obj.CenterY全都改为obj.Y - obj.CenterY,当然CenterX如法炮制。
11.加了一个isMoving,用来控制将每秒都计算坐标修改为每次点击到运动结束。
12.貌似SL中不能控制DispatcherTimer优先级别
13.更新了精灵拾取方法SilverlightControl20_MouseMove,原有方法的for循环中比较次数太多
14.将VLift数组修改为VLift和VLifeMax两个属性
15.动态障碍物的修改
1)将martrix修改为fixedObstruction,并增加varyObstruction。
2)将InitMartrix方法修改为InitObstruction,并在结尾增加语句:
varyObstruction = fixedObstruction.Clone() as byte[,];
3)将寻路算法AStarMoveTo、鼠标左击Carrier_MouseLeftButtonDown方法、检查周边障碍物的WillCollide方法中的变量fixedObstruction,修改为varyObstruction
4)修改WillCollide方法,扩大障碍物范围(引进HoldWidth和HoldHeight)
5)建立后台辅助线程
//设置后台线程 backgroundWorker = new BackgroundWorker(); backgroundWorker.DoWork += new DoWorkEventHandler(backWorker_DoWork); //设置游戏窗体辅助计时器 DispatcherTimer AuxiliaryThread = new DispatcherTimer(); AuxiliaryThread.Tick += new EventHandler(AuxiliaryThread_Tick); AuxiliaryThread.Interval = TimeSpan.FromMilliseconds(1000); AuxiliaryThread.Start();
16.升级到28章,图片路径变了
终于感受到SL中那么多函数,都是为了游戏开发而准备的了。
17.实现攻击动作
1)为spirit加上了若干属性。5维基本属性,以及20长度数组合成参数
2)修改spirit:
修改了Timer_Tick线程方法,当Action为Attrack的时候,调用新添加的DoInjure方法
添加了LockObject属性,获取或设置锁定的目标精灵对象
添加了AttackRange属性,获取或设置物理攻击范围(距离) ,暂时只是leader使用
添加了EffectiveFrame属性,获取或设置各动作产生实际效果的帧序号
ChangeAction方法,添加攻击的动作上去,速度即interval,需要再次重构
3)修改主窗体
统一了InitFacePlate方法(原先是2个),为此,给QXRoleFace增加了FaceSign、PKMode、VSName和VLevel四个属性
扩充了IsOpposition方法
添加了监视对象:string watchObj = null; 并相应修改了UpdateObjectFacePlate方法
修改了UpdateObjectFacePlate和UpdateLeaderFacePlate方法,为此向QXRoleFace添加了4个属性,没收拾干净,但基本够用了
添加了WillAttack方法,判断是否将要向锁定对象发起攻击
将方法RefreshObstruction修改为RefreshObstructionAndFacePlate,在其中执行UpdateObjectFacePlate和UpdateLeaderFacePlate方法,最后判断是否进入攻击范围,设置LockObject对象
LockObject和watchObj是一个东西,设计冗余了,删除watchObj,把LockObject升级为QXSpirit对象
添加SpiritEdge方法,根据发起攻击者朝向获取被攻击者实际脚底边缘(攻击寻路判断用)
在AuxiliaryThread_Tick线程方法中,这里代码有问题,不该在这里同步刷新主角攻击对象并启动寻路(CompositionTarget里面做这个事情)
重构了SilverlightControl20_MouseMove,原先的代码不好调试
扩充了寻路逻辑
考虑到MouseMove先于MouseLeftDown执行,所以在MouseLeftDown方法中,不需要再次进行命中测试,可以直接使用hitSpriteList这个集合。
在MouseLeftDown方法中,假如点到了自己,直接无视之。
奇怪,在window的ctor中,window的width还未设置。
做是做好了,但是人物第二次攻击就会原地不动,这个bug记下来,以后再处理。
18.即28_1
添加新的功能
1)禁止右键
2)QXDecoration真神奇,出场特效、脚底光环、鼠标点击光环,分别对应Image\Item下的2、6、1
鼠标点击光环属于不可移动障碍物,要在AllMove中一起移动
3)背景音乐
4)设置spirit头顶上伤害输出动画:-100或Miss
重构旧的代码
1)将Super.GetImage(@"/Images/Cursors/0.png")抽象为GetCursor方法
2)使用Point动画,而不再是Double动画
3)为了方便,为spirit添加了RootCanvas和ParentCanvas,前者是当前的根Canvas,后者是父亲Canvas
4)修改了WillCollide的边界控制,因为多了HoldWidth和HoldHeight
5)QXGame_Silverlight3.dll编译动作设置为none
6)重新扩充了AllMove方法,为了支持鼠标光标
7)WillCollide方法要重新,原先的有问题,比如说,spirit进入到怪物障碍物附近攻击,这时候再移动到别处,执行到Carrier_MouseLeftButtonDown方法的
else if (!WillCollide())
这一句的时候,会发现周围是“障碍物”,从而不会移动。要相应替换为:
//点到的地方不是障碍物才能移动
if (varyObstruction[(int)(p.X / gridSizeX), (int)(p.Y / gridSizeY)] != 0)
{
……
}
才正确。
8)spirit貌似始终挡在monster前面哦,这是因为我在InitMonster方法中没有设置怪物的ZIndex。
9)leader或monster伤血时,头顶上会显示值:-100或Miss。
由于原先把这个动态文字显示放到了主窗体中,所以它要随着spirit的移动而需要在AllMove方法中额外处理。
如今我把它放到QXSpirit中,将它的位置修改为:
X = 0,
Y = 0 - Math.Abs(spirit.DescriptionTop), //垂直居顶处理
这样就好了。
10)修复了攻击怪物贾函但是怪物郭晓颖伤血的bug
11)修改主窗体的CompositionTarget_Rendering方法,当进入攻击范围的时候,不再进行循环——通过将isMoving设置为false。
//结束循环
isMoving = false;
19.添加死亡效果,项目编号29
怪物篇:
1)在ChangeAction方法中,添加分支
case Actions.Death: Timer.Interval = TimeSpan.FromMilliseconds(300); CurrentStartFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2] + EachActionFrameRange[3]; CurrentEndFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2] + EachActionFrameRange[3] + EachActionFrameRange[4] - 1; OldAction = Actions.Death; break;
2)在DoInjure方法中,添加BattleHandle方法,进行战后处理
3)在spirit中,添加2个死亡标记:
bool isDeath = false; int deathDelay = 0;
为防止和主线程不同步,在spirit的Timer_Tick方法的一开始就要判断,一旦挂了,就保存它的尸体,6次循环过后,再火化(执行RemoveObject方法)。这期间就有足够的延迟时间了,不会发生null的异常事件。
if (isDeath) { deathDelay += 1; if (deathDelay == 6) { Super.RemoveObject(this, true); } return; }
同时,在spirit的Timer_Tick方法中添加分支,设置isDeath变量——死了:
case Actions.Death:
isDeath = true;
break;
4)深蓝色右手设置死亡的操作,即spirit.Action = Actions.Stop;我找不到
我是在BattleHandle方法中直接设置的,这样做可以让spirit停下来,而不是继续鞭尸。
20.让怪物动起来,项目编号29_1
1)在Super中,为所有spirit建立storyboard字典
Super不能啥都干啊,抽象出StoryboardRegFactory
2)在RefreshObstructionAndFacePlate中,把刷新动态障碍物方法抽象为UpdateMonsters,并在其中添加逻辑,侦测怪物SeekRange,锁定怪物的LockObject
3)因为怪物也有AttackRange,所以leader不需要有脚底障碍物,holdwifth和holdheight都是0
4)需要添加spirit参数的方法:WillCollide、AStarMoveTo、MoveTo
抽象出ArriveTarget和AllActive方法
5)把unitMoveCost升级为spirit的VRunSpeed属性,leader为70,怪物的从xml中读取
6)杀死怪物或者点击到别处,怪物面板消失,分别在CompositionTarget_Rendering和Carrier_MouseLeftButtonDown方法中判断
7)isMoving标记不再需要了,不划算,还不如每次都AllMove
8)发现一个bug,怪物死后再次点击尸体位置(鞭尸),会死在Carrier_MouseLeftButtonDown的
if (IsEfficaciousSection(hitSprite[i].EfficaciousSection, e.GetPosition(hitSprite[i])))
这里的hitSprite[i]就是死去的怪物,还没火化,所以会报异常,e.GetPosition处理不了尸体。
为此修改了GetHitSprite方法,
//从2开始,因为还有两个对象在最早被发现 for (int i = 2; i < hitElements.Count; i++) { if (hitElements[i] is QXSpirit29_1) { QXSpirit29_1 obj = hitElements[i] as QXSpirit29_1; if (obj.VLife != 0) hitSpriteList.Add(obj); } }
这样就只把没死的怪物加进hitSpriteList中
21.项目29_2,再次重构
添加新功能
1)导入经验值和升级体系
数据:在XML中添加LevelUp节点,为怪物添加经验值
创建:单件LevelUpExperienceSingleton
读取:在xml读取LevelUpExperienceList数组
分配:为leader设置VExperience和VExperienceMax
使用:在QXSpirit中添加EarnExperience方法
显示:更新leader面板中的level和exp,考虑到面板上的活力值Energy没有用,将其修改为经验值
重构旧代码
1)把gridSize拆分为gridSizeX和gridSizeY
2)fixedObstruction数组的维度设定是有章可循的,为此添加GetMatrixSize方法
3)QXSpirit的DiscriptionTop的初始化
leader.DescriptionTop = (leader.Body.Height - 160) / 2 - 30;
22.项目30 施放魔法
设定leader的magic为600
ActiveMonster
重新理清人物动作逻辑
貌似怪物死了 没有移除相应的storyboard
23.第3次重构
1)将doubleAnimation修改为PointAnimation,为此修改接口IObject
机制为:Coordinate属性一旦改变,就会修改坐标对象的Left和Top属性
这样就省去了额外再设置Left和Top,自动callback蛮好的。
累死我了,改了144处,还要改掉AStarMove和NormalMove中的X和Y关键帧动画
不能这样写:
this.Coordinate.Y -= 3;
因为Coordinate是一个Point,它是一个结构体
leader.Coordinate = new Point(x, y); 这会导致触发回调函数ChangeCoordinateCallback——使用leader的CenterX和CenterY,更新leader的Top和Left。
我们是否可以写成:
leader = new QXSpirit() { Name = "leader", Type = Spirit.Leader, Direction = direction, CenterX = 75, CenterY = 110, Coordinate = new Point(x, y),
};
为Coordinate设置值的时候,CenterX和CenterY已经有值了么?
测试发现,上述语句由于Coordinate = new Point(x, y),位于CenterX = 75, CenterY = 110, 之后,所以,触发回调函数ChangeCoordinateCallback的时候,CenterX和CenterY已经有值了。
如果把Coordinate = new Point(x, y),的位置上调到CenterX = 75, CenterY = 110, 之前,那么回调函数ChangeCoordinateCallback的触发会发生在CenterX = 75, CenterY = 110, 之前,而此时CenterX和CenterY还都是0。
同理,我要把ZIndex的设置放到 CenterX = 75, CenterY = 110, 之后
Left、Top也都不要了吧,ZIndex还需要
2)统一所有物体的坐标变量
3)将AllMove修改为Canvas移动
4)对Super进行重构,图像缓存独立出来,music扔回到主窗口,
5)重新规划IObject接口
6)去掉QXSpirit的Type属性
7)把魔法补齐,从xml读取数据,魔法伤害计算,右键点击与魔法施放位置调整
8)静态障碍物哪去了?