注:文章内容较长
一些回忆:
本人目前是大一新生,这也是我发的第一篇文章,还望多多指教。小时候非常喜欢4399和7k7k等小游戏平台,也不玩页游,于是小学周末放假,便与三两朋友,就着一台破电脑挨个试玩Flash游戏,非常有趣。之后学校开设flash动画的第二课堂,就果断参加,逐渐熟悉矢量图绘制(但绘画基础不牢靠)。之后又在图书馆找到一本用flash内置的ActionScript开发游戏的书,非常激动,连续借了好几个月,但由于不熟悉三角函数等基础知识,很多代码都是照着码上去的,但看着游戏跑起来,还是非常开心(个人认为,比隔壁Scratch开心一百倍!)。
应该就是这本,《ActionScript3.0游戏编程》
对Flash的一些个人看法
从最初的Flash10,到Flash CS5,再到Flash CS6,最后到现在的Adobe Animate 2022,每次更新换代,都把它们拖到桌面上相同的位置,像是专门留了把椅子(我几乎在与这款软件一起成长......)。从单纯的绘画,再到动画,再到脚本的编程,自己也在平日里一点一点地学习。要说一点看法没有,那是不可能滴。以下便是一点点个人看法,由于对其他软件接触不深,可能会有偏颇。
Flash界面清爽,既适合绘画,也适合在帧上直接写下脚本,美术和程序在我(年幼无知的)眼里结合得相当不错。且在学习面向对象编程的时候,在每一个影片剪辑中都可以编写相应的类,每个对象直接拖动、命名,非常地形象可视;且在界面布局方面,既可以直接在舞台上编辑,也可以写脚本批量控制,在我看来非常方便。
Flash CS5更新了绑定骨骼功能。对于无精力画逐帧动画的人来说,真的非常好用!
Adobe Animate更新了摄像头功能。终于,在有了摄像头以后,一些视角跟踪的游戏可以实现了!
但也有一些遗憾,比如基于HTML Canvas的flash文档,事实上,直接使用js开发,搭配canvas的拓展组件或许会更方便;又比如相关参考资料不多,而且打开偏慢。这次游戏制作主要看的资料不过这两个:Animate中的摄像头,Adobe ActionScript3 API参考,有些功能,如Vector等的使用,还要自己摸索,或者求助于隔壁javascript的相关问题。
开发记录
开发草案(或者叫策划?)
1. 故事构思
7k7kRaze2讲述了人类特种部队(Raze)抵抗异形入侵的故事.在这个过程中,主角需要化解丧尸,失控机器人等一系列灾难,最后与异形抢夺紫色能石blabla现在异形控制了赛博城里随处可见的携带激光武器的无人机,我们的Raze要在复杂的城市地图中将它们清扫干净
2. 素材绘制:
动态漫剧情介绍
游戏界面和菜单游戏场景设计:后景、动态元素、近景游戏地图设计玩家骨骼动画敌人动画枪械子弹
3.核心逻辑:平台动作游戏
玩家操作:
a和d键:摁下时设置x轴左右移动速度,抬起时结束移动。注意结束时动画应回到第一帧暂停。
w:与地面接触,触发一段跳,设置y轴向上速度,在跳起或腾空时再按下可触发二段跳。
s:触发下蹲动作,此时无法左右移动
摄像头跟踪玩家以及抖动特效:
需调用库:fl.VirtualCamera;
追踪方法:cameraObj.pinCameraToObject(getChildByName("InstanceName"), offsetx,offsety);其中offset可设置爆炸时抖动的特效,搭配setTint实现色调变白
碰撞检测:高级检测方式有两种,位图检测(bitmap,精细度高,用于复杂图形,且像素点不能太多)
网格检测(grid,更迭次数少,用于大量图形检测);但我都不会,本次地图与玩家碰撞就用矩形碰检吧哈哈哈。只需要利用速度提前判断下一帧的位置是否与某墙壁相碰,是则下一帧位置与墙贴合,再根据需要编写贴合、反弹、消失等行为
子弹运动逻辑:由易到难
线性激光类武器:子弹线形,无延迟,只需要判断该射线与地图矩形的碰撞位置,并在该点截断激光即可
反弹激光:在原有激光的基础上碰墙反弹,可以使用队列描述轨迹
普通球型子弹:速度慢,轨迹为抛物线,碰墙反弹。
范围型武器:碰墙爆炸,产生范围伤害,使用队列描述尾迹
跟踪类子弹:速度偏慢,逻辑为追逐行为。
弹刀特性:玩家抽出用激光刀划出优美(X)的圆,使得敌人子弹变为自己的子弹,用中学知识即可计算出反射向量。

4.游戏界面跳转安排:略
开发过程
1.坑:坐标系变换。若直接获取影片剪辑中的子元件坐标(如士兵对象中的子对象枪的坐标),得到的是它相对于容器(即士兵对象原点,通常是左上角)的坐标。若要得到相对于舞台的坐标,需要用localToGlobal方法:
var gunPoint:Point = (容器)player.localToGlobal
(new Point((子对象x)player.gun.x , (子对象x)player.gun.y) ));
且如果存在对象嵌套(如player.gun.shootingpoint),只需要用一次该方法即可转化为舞台坐标系下的坐标。
但是,当启用摄像头时,舞台会跟踪玩家移动,不是一个静止的参考系。因此,需要再将舞台坐标,转化为相对于场景中任意一个对象(通常在原点位置放一个点对象,可命名为basePoint,专门用于定位)的坐标:
var realGunPoint:Point = basePoint.globalToLocal(gunPoint);
2. 坑:使用脚本控制与原动画的冲突。像Raze游戏一样,需要实现让玩家的头、手和枪都实时朝向鼠标的功能,所以需要实时更改它们的rotation属性。但这时,它们就不会跟随原来身体一起下蹲了!出现了经典的分头行动。。。。。。尽管动画改变的只是x和y属性,并没有动用rotation,但脚本好像“接管”了对它们的控制。。。。。。最后苦思无果,只能用很笨的方法,即在脚本上再跟随帧数,控制它们的x,y,实现下蹲

正在被敌机包围中
3.有趣的环节:敌人行为逻辑的设计
生成点在楼顶,若与玩家距离小于开火区,用线段检测中间是否有障碍阻隔,无则发射激光
一开始没用寻路,敌人就朝着玩家前进,遇到墙往回弹一下,沿切向走。为了不被玩家轻易打死,会在距离小于一定范围时缓慢绕行。但这样敌人还是容易被墙卡住,不会主动到身旁。
所以用astar寻路算法,首先用二维网格划分场景,其中考虑敌机身长体宽,将障碍物的边界作扩增处理,如果点在矩形内,则将该处格点标为1,其余可走路径,标为0

接着设计节点类,存储当前坐标、父节点、出发点到当前点的代价h,曼哈顿距离(四个移动方向)或欧几里得距离(八个移动方向)g,设置评估方法f=h+g
class Node {
private var now: Point;
private var dad: Node = null;
private var g: uint;
private var h: uint;
//构造函数,不一定有父节点
public function Node(nowpoint: Point, target: Point) {
this.now = nowpoint;
this.h = 0;
this.calg(target);
}
public function seth(pare: Node, cost: uint):void {
this.dad = pare;
this.h = this.dad.h + cost;
}
public function getdad():Node{
return this.dad;
}
public function F(): uint {
return this.g + this.h;
}
public function H(): uint {
return this.h;
}
public function P(): Point {
return this.now;
}
//八个方向走,计算欧几里得距离,
//四个方向则计算曼哈顿距离
private function calg(target: Point) {
var disx:uint = Math.abs(target.x - this.now.x);
var disy:uint = Math.abs(target.y - this.now.y);
//this.g = disx+disy;
this.g = (disx>disy)?((disx-disy)*10+disx*14):((disy-disx)*10+disy*14);
}
}
最后编写astar算法,设置open和close两个数组,每次在open数组中选取f最小的节点展开并踢到close数组中。遍历展开的4个或8个节点:若已在open中,则判断新展开节点从出发点到达此处的代价是否更小,在open中保留代价更小的那个节点;若在close中,表示已经走过,不保留这个展开节点;否则这个节点是新的,加入open中。这样逐步展开,直到找到终点或没有可以展开的节点为止。
public var route: Array;//存储结果
private const stepx:Array = [-1,0,0,1,1,1,-1,-1];//每一步可以沿八个方向展开
private const stepy:Array = [0,-1,1,0,1,-1,1,-1];
//查询是否到过,针对closelist当中的节点
private function iswent(list:Array,tx:int,ty:int):Boolean{
for(var q=0;q<list.length;q++){
if(int(list[q].x) == tx && int(list[q].y) == ty){
//trace("went!",tx,ty);
return true;
}
}
return false;
}
//查询是否展开过且自动更新,针对openlist当中的节点
private function isopen(nodelist:Array,newnode:Node):Boolean{
for(var q=0;q<nodelist.length;q++){
if(nodelist[q].P().x == newnode.P().x &&
nodelist[q].P().y == newnode.P().y){
if(nodelist[q].H() > newnode.H()) nodelist[q] = newnode;
return true;
}
}
return false;
}
//维持一个优先队列,f小的放前面
private function insert(list:Array,newnode:Node):void{
var index:uint=0;
while(index<list.length && list[index].F()<newnode.F()) index++;
list.insertAt(index,newnode);
}
//每个敌人的astar每N秒一更新,将更新route
public function astar(grid: Array, sta: Point, end: Point):void{
//越界判定
if(sta.x<0 || sta.x>=grid[0].length ||
sta.y<0 || sta.y>=grid.length ||
end.x<0 || end.x>=grid[0].length ||
end.y<0 || end.y>=grid.length
){
return;
}
this.route = new Array();
//存储目前刚展开的节点,即开始列表
var que: Array = new Array();
que[0] = new Node(sta,end);
//存储关闭节点,即关闭列表
var close:Array = new Array();
do{
//选择F最小的节点展开,在优先对列中只需取第一个元素,并放入关闭节点中,只保存其坐标
var tnode:Node = que.shift();
close.push(tnode.P());
//打开邻近节点
for(var q=0;q<this.stepx.length;q++){
var nowx = tnode.P().x+stepx[q];
var nowy = tnode.P().y+stepy[q];
//筛选可以展开的节点,不越界,无障碍,未走过,未展开
if(nowx>=0 && nowx<grid[0].length
&& nowy>=0 && nowy<grid.length
&& grid[nowy][nowx] == false
&& !iswent(close,nowx,nowy)
&& !isopen(que,tnode)){
//trace("open!",nowx,nowy);
//终点判定,开始回溯路径,路径倒序存储,最后reverse
if(nowx == end.x && nowy == end.y){
this.route.push(end);
this.route.push(tnode.P());
while(tnode.getdad() != null){
tnode = tnode.getdad();
this.route.push(tnode.P());
}
this.route.reverse();
//强制退出;
que = [];
break;
}
var temp:Node = new Node(new Point(nowx,nowy),end);
temp.seth(tnode,(q<=3 ? 10:14));
//真·插入排序
insert(que,temp);
}
}
}while(que.length != 0);
}
最后,每帧都将route中的第一个节点当成敌机的目标,到达附近后shift()删除第一个节点,敌人便可以一直追着玩家满地图跑了

每过几秒,敌人的路径就会指引到玩家身边的节点
写在最后
使用多年,个人还是很喜欢用Animate的。希望能向各位读者大佬学习,热情不减,走向一个又一个未知的领域!