引言
整个游戏的推进工作显得异常顺利,或许之前我们所做的一切都是为了接下来的核心内容打基础:RPG之战斗系统。在踏入这块神秘而又让人的垂涎的领域前,我们迫切需要充实更多的相关知识以适应并更好的处理即将艳丽上演的战斗系统。
11.1游戏中的脚本与精灵捕获 (交叉参考:与嵌入式脚本语言Lua & JavaScript的交互(上) 与嵌入式脚本语言Lua & JavaScript的交互(下) 梦幻西游(Demo) 之 “天人合一”② 完美捕获精灵之神器 -- HitTest)
混沌中不知该从何而起,我们不妨重温一下前人走过的路:传统游戏开发的精髓都从哪些方面得以体现?稳健的核心引擎亦或是强大的编辑器?迷茫中我们似乎忽略了那些更为活跃而崇尚自由主义的对象:脚本。
游戏脚本(引擎)并不神秘,它的编写与游戏逻辑代码非常类似,主要区别在于游戏脚本相对于游戏主体引擎而言是外置的。那么何谓游戏主体引擎?即游戏中核心的基础功能,比如RPG中的角色、战斗、魔法实现、任务实现等;又比如SLG中的场景地图拼接实现、养成实现等等。如果将游戏的主体引擎比作一个人的躯体,那么衣服、鞋帽、饰品、握具、妆束等等则是该主体引擎的脚本,因为它们可以随意的取舍、替换,从而装扮出各式造型并完全独立于躯体之外。若以前,一款游戏(引擎)动辄上百万行代码,外加当时的编译器相对落后,如果所有逻辑代码均以硬编码的形式写在主体引擎中,将来就算是一丁点的BUG修复而做的代码改动,也将导致浪费大量的时间去重新编译。可见脱离游戏主体引擎的脚本代码具有最高的灵活性,同时也是打造基于松散耦合游戏框架所必需的主要成员之一。
游戏脚本可分为服务器端脚本和客户端脚本,在游戏开发中所涉猎的领域很多。我们不妨仔细分析下:一款游戏,哪些部分是可以替换的?界面(皮肤)?角色?魔法?剧情?人工智能(AI)?等等?。历史的长河见证了:将《剑侠情缘2 ONLINE》的界面换一套就诞生了《剑侠世界 ONLINE》;将《大话西游2 ONLINE》的角色和魔法换一套就催生了《大话西游3 ONLINE》;将《火焰纹章之 黑暗龙与光之剑》的剧情换N套就繁衍出了《火焰纹章》全系列;将《三国志I》的AI换上N套就衍变出了《三国志》全系列等等。可见,游戏中一切可以动态处理(标记、配置、变换)的对象,都可以将之定义成脚本(文件),就好比前面提到的:界面脚本、剧情脚本、AI脚本等等。
由此,游戏脚本的好处不言而喻,这也是为什么国内外的游戏开发者更热衷于游戏脚本编程的原因:在那些高度成熟的著名开源游戏引擎框架下,仅对脚本进行替换或重写即可创生出一款全新的游戏,惟有用“暴力的生产力”来形容这一美妙过程更加贴切。
现在,大家应该对游戏引擎和脚本及它们的关系有了大致了解;接下来进入正题:我们该如何将脚本技术具体的应用到课程示例游戏(引擎)中?
Silverlight能够支持的脚本目前已达10余种之多,以Javascript、IronRuby、IronPython为主流。而作为HTML配偶的Javascrpit与生俱来即有对Silverlight的高度兼容,无须额外的DLL支持,相当默契。实现起来就更加简单了,仅仅一句就可轻松调用:
HtmlPage.Window.Invoke(……);
可千万别小看了这句话,请注意看上图。首先,它调用的方法名是string类型;其次,它的赋值参数是object[] 类型;最后,它返回的是object类型,当然,也可以直接作为void方法用。
懂行的朋友一看就明白,这句方法封装得真TMD完美:只要知道Javascprit编写的方法(函数)名就可以调用,比反射写起来还要简单;参数随意传递,自由解析;返回值同样,一切的自由全交与开发者发挥,太棒了,这才是主体逻辑与脚本逻辑完美分离所必须滴。
Silverlight中居然有这么厉害的脚本解析方法,似乎不用它来点锦上添花感觉对不起广大读者。于是乎想到了游戏中怪物的刷新与出现,若能通过脚本编写独立的刷新位置、阵型逻辑,那么未来制作怪物、NPC的其他AI逻辑将显得游刃有余。绞尽脑汁再一次拿起被我鄙视过无数次而又迫于无奈的Javascript,吃力的编写了以下绘制各类图形的函数Script.js:
function DiamondPath(centerX, centerY, arg, limit) {
var x = Math.random() * arg + centerX - arg;
var y = Math.random() * arg + centerY - arg;
return Validate(x, y, limit);
}
//圆形路径点
function RoundPath(centerX, centerY, arg, limit) {
var angle = Math.random() * 360;
var x = Math.sin(GetRadian(angle)) * arg + centerX;
var y = Math.cos(GetRadian(angle)) * arg + centerY;
return Validate(x, y, limit);
}
//正弦路径点
function SinePath(centerX, centerY, arg, limit) {
var x = Math.random() * ((4 * Math.PI) - (-4 * Math.PI)) + (-4 * Math.PI);
var y = Math.sin(x) * arg;
x = x * arg + centerX;
y = y + centerY;
return Validate(x, y, limit);
}
//螺旋路径点
function EddyPath(centerX, centerY, arg, limit) {
var angle = Math.random() * (360 * 4);
var x = angle * Math.cos(GetRadian(angle)) / arg + centerX;
var y = angle * Math.sin(GetRadian(angle)) / arg + centerY;
return Validate(x, y, limit);
}
//五角星路径点
function StarPath(centerX, centerY, arg, limit) {
var x, y;
var cx = 15 * arg;
var cy = 8 * arg;
var pointX = new Array(5);
var pointY = new Array(5);
for (i = 0; i < 5; i++) {
var dAngle = (i * 0.8 - 0.5) * Math.PI;
pointX[i] = cx * (0.25 + 0.24 * Math.cos(dAngle));
pointY[i] = cy * (0.5 + 0.48 * Math.sin(dAngle));
}
var n = parseInt(Math.random() * 5);
var m = (n + 1) == 5 ? 0 : (n + 1);
if (pointX[n] > pointX[m]) {
x = Math.random() * (pointX[n] - pointX[m]) + pointX[m];
} else {
x = Math.random() * (pointX[m] - pointX[n]) + pointX[n];
}
y = ((pointY[m] - pointY[n]) / (pointX[m] - pointX[n])) * (x - pointX[n]) + pointY[n] + centerY;
x = x + centerX - cx / 5;
y = y - cy / 2;
return Validate(x, y, limit);
}
//获取角度的弧度值
function GetRadian(angle) {
return angle * Math.PI / 180;
}
//验证x,y是否越界
function Validate(x, y, limit) {
if (x < 0) { x = 0; } else if (x > limit) { x = limit; }
if (y < 0) { y = 0; } else if (y > limit) { y = limit; }
return [x, y];
};
虽然调试时令我几近崩溃,太不友好了恨不得有空就揣上几脚,或许今后当我开发WP7游戏时才可放肆的忘掉这段伤心的往事,尔尔。
一切就绪,接下来是实际测试:
comboBox0.Items.Add(new ComboBoxItem() { Tag = "RoundPath,10", Content = "圆形", IsSelected = true });
comboBox0.Items.Add(new ComboBoxItem() { Tag = "SinePath,2", Content = "正弦" });
comboBox0.Items.Add(new ComboBoxItem() { Tag = "EddyPath,100", Content = "螺旋" });
comboBox0.Items.Add(new ComboBoxItem() { Tag = "StarPath,3.33", Content = "五星" });
button4.Click += (s, e) => {
AddTestSprites(spriteCount);
};
/// <summary>
/// 添加测试精灵
/// </summary>
/// <param name="num"></param>
void AddTestSprites(int num) {
for (int i = 0; i < num; i++) {
Sprite sprite = new Sprite() {
......
};
......
scene.AddSprite(sprite);
sprite.RunRandomMoveScript(((ComboBoxItem)comboBox0.SelectedItem).Tag.ToString(), hero.Coordinate, Scene.TerrainMatrix.GetUpperBound(0));
}
}
其中精灵的RunRandomMoveScrpit方法如下:
/// 运行图形随机点移动脚本(测试调用Javascript脚本)
/// </summary>
/// <param name="arg">参数</param>
/// <param name="center">中心</param>
/// <param name="limit">边界上限</param>
public void RunRandomMoveScript(string arg, Point center, int limit) {
string[] args = arg.Split(',');
if (Application.Current.IsRunningOutOfBrowser) {
......
} else {
Array array = ((ScriptObject)HtmlPage.Window.Invoke(args[0], center.X, center.Y, args[1], limit)).ConvertTo<Array>();
StraightRunTo(new Point(Convert.ToDouble(array.GetValue(0)), Convert.ToDouble(array.GetValue(1))));
}
}
够简单吧,大家可以将C#编写的所有代码当作游戏的主体逻辑,根据以上例子,我们仅需为精灵定义好一个RunRandomMoveScript方法,并在其中放上HtmlPage.Window.Invoke这句神马级的调用方法,那么前面外置的Script.js文件中的所有函数都将可以随意更改其内部逻辑而无须重新编译,更直接的好处还有当用户每次登陆网站时,他们无须再次下载那庞大的XAP或DLL,浏览器会自行选择最新的js文件使用,这也是提升用户体验的良好途径,低碳且绝对环保。
然而需要特别注意的是,Javascript与HTML的结合过度紧密即意味着一旦脱离(或被浏览器禁用)将显得无所适从。当我们在OOB模式运行时会发现一切功能都毫无反映,道理可想而知。因此,在做项目策划与需求分析时尽可能的着眼于此项目未来将要或可能会运行的平台设备;在外置脚本的选择上必须考虑是否能做到兼容或无缝移植,否则极易功亏一篑。其实我们完全可以通过反射的方式来解析(驱动)由C#编写的(动态下载或加载的)DLL脚本,每次修改逻辑仅需对独立的C#脚本项目进行重新编译并配置,而无需重新编译整个游戏主引擎项目(XAP)。
最后又到了欣赏劳动成果的时候了,通过数学图形公式我们可以创造出千变万化的精灵阵型,真的很酷:
无论是刷怪模式、走位路径还是阵型样式,都可以通过脚本一一实现;包括清怪时怪物消失产生的蒸发动画所描绘出的图形,大家是否能从中找到魔法组合的灵感?没错,比如一个火焰动画素材配合上不同的数学图形公式,可打造出层出不穷的华丽魔法,这才是游戏编程的艺术,让多学科知识在同一个领域上交融,中国未来的游戏界必将因为您的加入而引领革命性的技术风暴,我们风雨与共携手并进!!
峰回路转,话说,离战斗已越来越近,敌人的铁蹄声此起彼伏,似乎要为这场壮烈的战役做些实质性的准备了。与敌人战斗或与NPC交互,首先需要能通过操作获取指定的对象;在Silverlight游戏中我们称之为捕获精灵,通俗来讲即通过命中测试的方法找到鼠标所指向位置的特定精灵:
/// 通过命中测试获取点中的精灵们
/// </summary>
List<Sprite> HitSprite(Point p) {
List<UIElement> hitElements = VisualTreeHelper.FindElementsInHostCoordinates(p, scene) as List<UIElement>;
List<Sprite> hitSprites = new List<Sprite>();
for (int i = 0; i < hitElements.Count; i++) {
if (hitElements[i] is Sprite) {
hitSprites.Add(hitElements[i] as Sprite);
}
}
return hitSprites;
}
Sprite tempSprite; //上一次命中的精灵
/// <summary>
/// 游戏中鼠标移动
/// </summary>
void LayoutRoot_MouseMove(object sender, MouseEventArgs e) {
//移动图形鼠标
Point p = e.GetPosition(this);
cursor.Coordinate = p;
//首先判断是否命中某个精灵
List<Sprite> hitSprites = HitSprite(e.GetPosition(LayoutRoot));
bool IsHitSprite = false;
for (int i = 0; i < hitSprites.Count; i++) {
//鼠标位置为精灵有效实体区域才算命中该精灵
if (hitSprites[i].InPhysicalRange(e.GetPosition(hitSprites[i]))) {
//假如命中的精灵于前一选中精灵不同则更换选中精灵
if (tempSprite != hitSprites[i]) {
if (tempSprite != null) { tempSprite.BodyEffect = BodyEffects.None; }
tempSprite = hitSprites[i];
//假如主角与目标阵营敌对则更换鼠标指针为攻击图形
if (hero.IsHostileTo(hitSprites[i].Camp)) {
cursor.CursorType = CursorTypes.Attack;
hitSprites[i].BodyEffect = BodyEffects.Red;
} else {
cursor.CursorType = CursorTypes.Normal;
hitSprites[i].BodyEffect = BodyEffects.Green;
}
}
IsHitSprite = true;
break;
}
}
//假如没有命中任何精灵则将前一选中精灵取消命中效果
if (!IsHitSprite) {
if (tempSprite != null) { tempSprite.BodyEffect = BodyEffects.None; tempSprite = null; }
cursor.CursorType = CursorTypes.Normal;
}
}
就如上面代码所示,按照传统RPG游戏开发思路,当鼠标经过某精灵时,该精灵将会被高亮突出显示,起到友好提示的作用;当在某精灵上点击时,该精灵则会被一个光环萦绕,提示玩家目前所操作的具体精灵对象。所有的精灵特效逻辑都写在Sprite.cs控件中,具体细节大家可从源码中自行参阅。
本课小结:对比本节那些用于描述路径点(图形)的脚本,精灵、动画、场景等的xml配置文件也同样可看作脚本的一种,就好比任务的实现大多会组合用到AI逻辑脚本和剧情内容脚本等等。另外,游戏引擎框架可大致划分为主体引擎和脚本引擎,说脚本引擎在游戏中的地位与主体引擎平起平座毫不言过。游戏开发中一切能够被分离出来的逻辑、配置均可放在脚本中处理与保存,最大程度的模块化游戏引擎框架,非常有利于团队的协同开发以及后续维护,乃至可能的面向社会所有游戏开发者的开源拓展等。
本课源码:点击进入目录下载
参考资料:中游在线[WOWO世界] 之 Silverlight C# 游戏开发:游戏开发技术
教程Demo在线演示地址:http://silverfuture.cn