Silverlight MMORPG网页游戏开发课程[一期] 第二课:完美2D精灵
引言
通过上一课的学习我们掌握了Silverlight中实现对象动画的3种方式。大家需要特别注意在Silverlight中鼠标左键按下、台起;鼠标右键按下、台起以及鼠标滚轮滚动这5 个事件都是路由的,路由功能在游戏及动画开发领域用处巨大,后续课程我会专门找时间来详细讲解。
光会移动的一张静止图片还称不上精灵,加入它自身的动作动画并按实际情况呈现出来才叫完美。我们该如何让精灵“活”起来呢?本节我将为大家介绍两种实现精灵动作动画的方法。
2.1通过切割(Clip)加偏移(TranslateTransform)实现精灵动作动画(交叉参考: 完美精灵之八面玲珑 GPU硬件加速下Silverlight超性能动画实现(下) 三国策(Demo) 之 “江山一统”①)
首先我们规定好精灵的朝向数。传统RPG游戏中的精灵以4方向和8方向的情况居多,当然还有2方向(横版游戏)的。然后我们需要选择最佳的精灵动作动画实现方式,1.3中提到了可以通过DispatcherTimer来实现可控的精灵动作动画,然后配合使用Clip+TranslateTransform对精灵整图素材进行切割并偏移,我称之为:矩形截取。
课程中以8方向的精灵为例,在1.1的源码的基础上(精灵对象使用1.3中的Image控件),我们先将一张按类似下图有规律布局并事先处理好的精灵序列帧整图添加进游戏项目资源中。
接下来定义一个用于动作动画播放的DispatcherTimer对象(参考1.3):
/// <summary>
/// 播放精灵动作动画用的计时器
/// </summary>
DispatcherTimer dispatcherTimer = new DispatcherTimer() {
Interval = TimeSpan.FromMilliseconds(120)
};
public MainPage() {
InitializeComponent();
……
//开始播放精灵动作动画
dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
dispatcherTimer.Start();
}
接着按照精灵动作一帧图片接一帧图片循环切换的原理,简单的编写出Tick事件中的相应逻辑:
int currentFrame = 0; //当前帧
int startFrame = 0; //开始帧
int endFrame = 5; //结束帧
int direction = 0; //朝向
const int singleWidth = 150; //精灵实际宽
const int singleHeight = 150; //精灵实际高
private void dispatcherTimer_Tick(object sender, EventArgs e) {
//假如当前帧大于结束帧,则归0重新播放
if (currentFrame > endFrame) { currentFrame = startFrame; }
//按精灵单张图片尺寸并根据帧的位置进行截取,后再将截取的部分偏移回原点
double translateX = singleWidth * currentFrame,
translateY = singleHeight * direction;
sprite.Clip = new RectangleGeometry() {
Rect = new Rect() {
X = translateX,
Y = translateY,
Width = singleWidth,
Height = singleHeight,
}
};
sprite.RenderTransform = new TranslateTransform() { X = -translateX, Y = -translateY };
currentFrame++;
}
精灵的8个朝向(以北为0,顺时针方向旋转依次1、2、3、4、5、6、7)需要按照游戏运行时它移动的起点与终点之间的斜角来判断获取,我写的算法如下:
/// <summary>
/// 计算当前坐标与目标点之间的正切值获取朝向
/// </summary>
/// <param name="current">当前坐标</param>
/// <param name="target">目标坐标</param>
/// <returns>朝向代号</returns>
public int GetDirection(Point current, Point target) {
double tan = (target.Y - current.Y) / (target.X - current.X);
if (Math.Abs(tan) >= Math.Tan(Math.PI * 3 / 8) && target.Y <= current.Y) {
return 0;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && target.X > current.X && target.Y < current.Y) {
return 1;
} else if (Math.Abs(tan) <= Math.Tan(Math.PI / 8) && target.X >= current.X) {
return 2;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && target.X > current.X && target.Y > current.Y) {
return 3;
} else if (Math.Abs(tan) >= Math.Tan(Math.PI * 3 / 8) && target.Y >= current.Y) {
return 4;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && target.X < current.X && target.Y > current.Y) {
return 5;
} else if (Math.Abs(tan) <= Math.Tan(Math.PI / 8) && target.X <= current.X) {
return 6;
} else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && target.X < current.X && target.Y < current.Y) {
return 7;
} else {
return 0;
}
}
最后在鼠标左键按下事件LayoutRoot_MouseLeftButtonDown中将点击点与精灵位置点带入以上方法计算出精灵新的朝向并移动:
private void LayoutRoot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
//获取点击处相对于容器的坐标位置
Point p = e.GetPosition(LayoutRoot);
//计算精灵朝向
direction = GetDirection(new Point(Canvas.GetLeft(sprite), Canvas.GetTop(sprite)), p);
……
}
2.2通过图片轮换实现精灵动作动画(交叉参考:完美精灵之八面玲珑① 完美精灵之八面玲珑② 完美精灵之八面玲珑③ 完美移动)
大家小时候是否看过“翻纸动画”?2D游戏中我们同样可以利用类似原理将一系列帧图片按顺序播放出来以实现动画效果。因此我们首先需要准备好精灵各方向各动作的各帧图片并添加进项目资源中:
上图只是该精灵所有动作帧图片素材的局部,按照个人习惯我对它们以:动作代号-朝向代号-帧号的形式统一规范命名。由此编写出相应的Tick事件逻辑如下(我在Silverlight中暂时找不到现成的方法,这里通过ProjectName方法来获取项目名,目的是读取项目资源路径时动态且可移植性更强。如哪位朋友能提供直接获取项目名的方法,望留言指点):
/// <summary>
/// 精灵
/// </summary>
Image sprite = new Image();
/// <summary>
/// 获取Silverlight项目名(或许还有更直接的办法)
/// </summary>
/// <returns>Silverlight项目名</returns>
private string ProjectName() {
string[] value = Application.Current.Host.Source.AbsolutePath.Split('/');
return value[value.GetUpperBound(0)].Replace(".xap", "");
}
int currentFrame = 0; //当前帧
int startFrame = 0; //开始帧
int endFrame = 3; //结束帧
int state = 1; //状态代号
int direction = 0; //朝向代号
private void dispatcherTimer_Tick(object sender, EventArgs e) {
//假如当前帧大于结束帧,则归0重新播放
if (currentFrame > endFrame) { currentFrame = startFrame; }
//根据当前帧位置轮换精灵图片
sprite.Source = new BitmapImage(new Uri(string.Format(@"/{0};component/Res/Sprite/{1}-{2}-{3}.png", ProjectName(), state, direction, currentFrame), UriKind.Relative));
currentFrame++;
}
正常的精灵移动应该是像1.2/1.3那样的匀速移动,因此我们可以在精灵每次移动时通过计算出起点(start)与终点(end)间的距离再乘上速度系数(speed)作为Storyboard的Duration,从而使得精灵移动起来更为均匀且自然:
double speed = 4; //单位速度
private void LayoutRoot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point start = new Point(Canvas.GetLeft(sprite), Canvas.GetTop(sprite));
//获取点击处相对于容器的坐标位置
Point end = e.GetPosition(LayoutRoot);
//计算精灵朝向
direction = GetDirection(start, end);
//计算目标与起点之间距离
int duration = Convert.ToInt32(Math.Sqrt(Math.Pow((end.X - start.X), 2) + Math.Pow((end.Y - start.Y), 2)) * speed);
//创建移动用的动画故事板
Storyboard storyboard = new Storyboard();
//创建X轴方向动画
DoubleAnimation xAnimation = new DoubleAnimation() {
//起点
From = start.X,
//终点
To = end.X,
//花费时间
Duration = new Duration(TimeSpan.FromMilliseconds(duration)),
};
//将X轴方向动画赋予rectangle
Storyboard.SetTarget(xAnimation, sprite);
//设置X轴方向动画所影响的对象属性为Canvas.Left
Storyboard.SetTargetProperty(xAnimation, new PropertyPath("(Canvas.Left)"));
//将X轴方向动画添加进动画故事板中
storyboard.Children.Add(xAnimation);
//创建Y轴方向动画
DoubleAnimation yAnimation = new DoubleAnimation() {
From = start.Y,
To = end.Y,
Duration = new Duration(TimeSpan.FromMilliseconds(duration)),
};
Storyboard.SetTarget(yAnimation, sprite);
//设置Y轴方向动画所影响的对象属性为Canvas.Top
Storyboard.SetTargetProperty(yAnimation, new PropertyPath("(Canvas.Top)"));
storyboard.Children.Add(yAnimation);
//播放动画
storyboard.Begin();
}
另外,大家是否有注意2.1的Demo中当我们在画布里点击时,精灵移动的起点与终点均对应的是当时精灵图片的左上角,这与真实情况并不相符。实际游戏中精灵向目标移动后最终脚底位置会站在目标点上(鼠标点击点)。于是我们还得为精灵添加一个center属性用于描述其脚底中心位置,每次点击得到的目标点X、Y值还得分别减去center的X、Y值:
private void LayoutRoot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point start = new Point(Canvas.GetLeft(sprite), Canvas.GetTop(sprite));
//获取点击处相对于容器的坐标位置
Point end = e.GetPosition(LayoutRoot);
end = new Point(end.X - center.X, end.Y - center.Y);
……
}
离完美精灵仅有一步之遥。目前的精灵一直处于跑步状态,我们该如何实现精灵多状态动作之间的切换呢?比方说精灵移动到目的地后即站立。当然我们得事先准备好精灵站立的素材并添加进项目资源中,这里我定义了一个默认值设置为0名叫state的变量(0代表站立、1代表跑动),然后在Storyboard的Completed事件中编写状态切换逻辑(由于Demo中精灵站立用4张图片;跑动用6张图片,因此在不同状态切换时还需要更新endFrame的值):
/// <summary>
/// 切换精灵状态
/// </summary>
/// <param name="newState">新状态</param>
private void ChangeStateTo(int newState) {
currentFrame = 0;
state = newState;
switch (newState) {
//站立
case 0:
endFrame = 3;
break;
//跑动
case 1:
endFrame = 5;
break;
}
}
最终我们鼠标左键点击控制精灵移动的方法如下:
double speed = 4; //速度系数
Storyboard storyboard = new Storyboard(); //移动动画故事板
private void LayoutRoot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point start = new Point(Canvas.GetLeft(sprite), Canvas.GetTop(sprite));
//获取点击处相对于容器的坐标位置
Point end = e.GetPosition(LayoutRoot);
end = new Point(end.X - center.X, end.Y - center.Y);
//计算精灵朝向
direction = GetDirectionByTan(end.X, end.Y, start.X, start.Y);
//计算目标与起点之间距离
int duration = Convert.ToInt32(Math.Sqrt(Math.Pow((end.X - start.X), 2) + Math.Pow((end.Y - start.Y), 2)) * speed);
//停止掉原先的移动动画故事板并注销掉相应事件以释放内存
storyboard.Pause();
storyboard.Completed -= storyboard_Completed;
//创建新的运动动画故事板
storyboard = new Storyboard();
//创建X轴方向动画
DoubleAnimation xAnimation = new DoubleAnimation() {
//起点
From = start.X,
//终点
To = end.X,
//花费时间
Duration = new Duration(TimeSpan.FromMilliseconds(duration)),
};
//将X轴方向动画赋予rectangle
Storyboard.SetTarget(xAnimation, sprite);
//设置X轴方向动画所影响的对象属性为Canvas.Left
Storyboard.SetTargetProperty(xAnimation, new PropertyPath("(Canvas.Left)"));
//将X轴方向动画添加进动画故事板中
storyboard.Children.Add(xAnimation);
//创建Y轴方向动画
DoubleAnimation yAnimation = new DoubleAnimation() {
From = start.Y,
To = end.Y,
Duration = new Duration(TimeSpan.FromMilliseconds(duration)),
};
Storyboard.SetTarget(yAnimation, sprite);
//设置Y轴方向动画所影响的对象属性为Canvas.Top
Storyboard.SetTargetProperty(yAnimation, new PropertyPath("(Canvas.Top)"));
storyboard.Children.Add(yAnimation);
//移动结束后切换成站立状态
storyboard.Completed += new EventHandler(storyboard_Completed);
//播放动画
storyboard.Begin();
//开始移动后切换成跑动状态
ChangeStateTo(1);
}
private void storyboard_Completed(object sender, EventArgs e) {
//移动结束时切换成站立状态
ChangeStateTo(0);
}
值得注意的一个地方:此时我将storyboard对象移到了LayoutRoot_MouseLeftButtonDown方法体外边,目的是每次点击目的地时首先停止掉(这里的停止用Pause而不是Stop)之前的storyboard故事板移动动画,然后再重新创建一个新的出来,这样做可以使得原先的动画不至于干扰到新的动画效果。比如在新的动画可能还未执行完时就触发了旧动画的Completed事件,这是不允许的,建议大家自行尝试体会一下。
另外,C#中的事件尽量还是不要写成匿名形式,在需要销毁某个对象时一定要注销掉它曾经注册过的所有事件才能完全释放掉所占用的内存,否则极易造成内存泄露,切记。(参考:http://msdn.microsoft.com/zh-cn/library/ms366768.aspx)
本课源码:点击进入目录下载
本课小结:通过矩形截取方式实现精灵动作动画优势在于素材布局方便、直观,实现小游戏相当方便;缺点就是性能消耗较大,不适合大规模同时使用。因此我更推荐通过图片轮换的方式来实现精灵动作动画。
课后作业:
作业说明:
参考资料:中游在线[WOWO世界] 之 Silverlight C# 游戏开发:游戏开发技术
教程Demo在线演示地址:http://silverfuture.cn