AniamtexActionScript3.0游戏复刻记录

注:文章内容较长

一些回忆:

本人目前是大一新生,这也是我发的第一篇文章,还望多多指教。小时候非常喜欢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的。希望能向各位读者大佬学习,热情不减,走向一个又一个未知的领域!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值