Flash游戏编程基础教程:用 ActionScript 控制角色移动

AI资讯3小时前发布 jdkjadmin
0 00
Flash游戏编程基础教程:用 ActionScript 控制角色移动

Flash游戏编程基础教程:用 ActionScript 控制角色移动

角色移动是许多游戏的基础功能:玩家按下方向键,角色便在舞台上移动;松开按键,角色停止。这个看似简单的功能,实际牵涉输入事件、帧更新、坐标系、速度计算和边界处理。把这些基础概念处理好,后续添加动画、碰撞检测或关卡系统时会更容易。

本文使用 ActionScript 3.0(AS3),以 Adobe Animate 中的 Flash 文档为例,完成一个可以运行的键盘移动示例。需要先说明运行环境的现实情况:Adobe Flash Player 已于 2020 年底停止支持,主流浏览器也不再运行其插件。因此,本文代码适合维护旧项目、学习 AS3、在兼容的离线环境中测试,或作为移植到其他运行时的基础;它不是让现代浏览器重新启用 Flash Player 的方法。

一、适用场景与实现思路

AS3 的键盘移动适用于仍需维护的 Flash 游戏、交互课件和离线演示,也适合学习事件驱动编程。典型场景是:用方向键或 WASD 控制一个显示对象在二维舞台上移动。

实现时需要区分两个时间点:

  • 按键事件:键盘按下或松开时,更新按键状态。
  • 帧更新事件:每帧根据当前按键状态计算位移,并更新角色坐标。

不建议只在 KEY_DOWN 事件中直接移动角色。按住一个键时,操作系统可能以一定频率重复发送按键事件;这个频率会因系统设置和运行环境而异,角色速度也就不容易保持一致。更可靠的做法是记录哪些键正处于按下状态,然后在帧更新中统一移动。

二、准备 Animate 文档

  1. 新建一个 ActionScript 3.0 文档,并保存为 MovementDemo.fla。
  2. 在舞台上放置一个角色图形,将其转换为影片剪辑(MovieClip)。
  3. 选中影片剪辑,在“属性”面板的实例名称栏中设置实例名为 player。实例名区分大小写。
  4. 建议将角色的注册点放在图形中心,便于理解后文的边界计算。注册点不在中心也可以运行,但坐标含义和边界策略要相应调整。
  5. 在文档属性中设置文档类为 Main,并在与 FLA 文件相同的源码目录下建立 Main.as。

文档类是 AS3 项目的入口之一。编译时,Animate 会把文档类关联到主时间轴。本例将角色放在主时间轴第一帧,因此可以通过 player 访问它。若实例名拼写不一致、实例不在主时间轴,或实例尚未创建,访问角色时可能出现运行时错误。遇到这类问题,应先检查时间轴层级、帧位置和实例名称。

三、可运行示例:方向键移动并限制在舞台内

将下面代码保存为 Main.as。代码实现方向键移动、斜向速度归一化、帧时间计算、舞台边缘限制,以及键盘监听器的清理。

package
{
    import flash.display.MovieClip;
    import flash.events.Event;
    import flash.events.KeyboardEvent;
    import flash.ui.Keyboard;

    public class Main extends MovieClip
    {
        // 角色必须是主时间轴上的实例,实例名为 player。
        public var player:MovieClip;

        private var leftDown:Boolean = false;
        private var rightDown:Boolean = false;
        private var upDown:Boolean = false;
        private var downDown:Boolean = false;

        // 以像素/秒表示速度;可按游戏手感调整。
        private const MOVE_SPEED:Number = 240;

        private var lastTime:int = 0;

        public function Main()
        {
            if (stage != null)
            {
                startGame();
            }
            else
            {
                addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
            }
        }

        private function onAddedToStage(event:Event):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
            startGame();
        }

        private function startGame():void
        {
            if (player == null)
            {
                throw new Error(
                    "找不到角色实例。请检查主时间轴上的实例名是否为 player。"
                );
            }

            // 请求焦点,让舞台接收键盘输入。
            stage.focus = this;

            stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
            stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);
            stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
            stage.addEventListener(Event.DEACTIVATE, onDeactivate);

            lastTime = getTimerValue();
        }

        private function onKeyDown(event:KeyboardEvent):void
        {
            setKeyState(event.keyCode, true);
        }

        private function onKeyUp(event:KeyboardEvent):void
        {
            setKeyState(event.keyCode, false);
        }

        private function setKeyState(keyCode:uint, isDown:Boolean):void
        {
            if (keyCode == Keyboard.LEFT)
            {
                leftDown = isDown;
            }
            else if (keyCode == Keyboard.RIGHT)
            {
                rightDown = isDown;
            }
            else if (keyCode == Keyboard.UP)
            {
                upDown = isDown;
            }
            else if (keyCode == Keyboard.DOWN)
            {
                downDown = isDown;
            }
        }

        private function onEnterFrame(event:Event):void
        {
            var now:int = getTimerValue();
            var elapsed:Number = (now - lastTime) / 1000.0;
            lastTime = now;

            // 限制单帧时间,避免切回窗口或卡顿后突然移动过远。
            if (elapsed > 0.05)
            {
                elapsed = 0.05;
            }

            var dx:Number = 0;
            var dy:Number = 0;

            if (leftDown)  dx -= 1;
            if (rightDown) dx += 1;
            if (upDown)    dy -= 1;
            if (downDown)  dy += 1;

            // 同时按两个方向时归一化,避免斜向速度更快。
            if (dx != 0 || dy != 0)
            {
                var length:Number = Math.sqrt(dx * dx + dy * dy);
                dx /= length;
                dy /= length;

                player.x += dx * MOVE_SPEED * elapsed;
                player.y += dy * MOVE_SPEED * elapsed;

                keepPlayerOnStage();
            }
        }

        private function keepPlayerOnStage():void
        {
            // 该计算假设角色注册点在图形中心。
            var halfWidth:Number = player.width / 2;
            var halfHeight:Number = player.height / 2;

            var minX:Number = halfWidth;
            var maxX:Number = stage.stageWidth - halfWidth;
            var minY:Number = halfHeight;
            var maxY:Number = stage.stageHeight - halfHeight;

            if (minX > maxX)
            {
                player.x = stage.stageWidth / 2;
            }
            else
            {
                if (player.x < minX) player.x = minX;
                if (player.x > maxX) player.x = maxX;
            }

            if (minY > maxY)
            {
                player.y = stage.stageHeight / 2;
            }
            else
            {
                if (player.y < minY) player.y = minY;
                if (player.y > maxY) player.y = maxY;
            }
        }

        private function onDeactivate(event:Event):void
        {
            // 窗口失去激活状态时,清空按键状态,避免松键事件丢失。
            clearKeys();
        }

        private function clearKeys():void
        {
            leftDown = false;
            rightDown = false;
            upDown = false;
            downDown = false;
        }

        private function getTimerValue():int
        {
            return flash.utils.getTimer();
        }
    }
}

代码中使用了 getTimer()。它返回运行时计时器的毫秒数,来源于 flash.utils。如果编译器提示找不到该函数,可以在导入列表中增加 import flash.utils.getTimer;,再将代码里的 getTimerValue() 方法改为直接调用 getTimer()。为避免依赖通配符或特定编译器的名称解析行为,也可以直接使用下面这种常见写法:增加该导入,并把 getTimerValue 方法调用替换为 getTimer(),再删除包装方法。

速度常量 MOVE_SPEED 的单位是像素/秒。每帧位移由 速度 × elapsed 得出。例如,速度设为 240 像素/秒,某帧经过约 1/60 秒,理论位移约为 4 像素。实际帧率可能波动,因此按实际经过时间计算,比“每帧固定移动若干像素”更能保持不同帧率下的速度接近。

四、理解代码中的关键部分

1. 用状态变量表示按键是否按下

leftDown、rightDown、upDown 和 downDown 分别记录四个方向。按下键时设置为 true,松开时设回 false。帧更新函数只读取这些状态,不需要在每次按键事件里直接修改角色位置。

方向键对应的是 Keyboard.LEFT、Keyboard.RIGHT、Keyboard.UP 和 Keyboard.DOWN。这些常量比直接写入键码更易读,也能减少混淆。键盘布局、操作系统和运行时可能影响某些按键的实际行为,因此在目标环境中仍需测试。

2. 为什么斜向移动需要归一化

若只按右键,速度向量为 (1, 0);若同时按右键和下键,未经处理的向量是 (1, 1),长度为 √2。这样斜向移动的距离会比单方向快约 41%。示例计算向量长度后,将两个分量都除以长度,使移动方向改变而速度大小保持一致。

归一化只在至少有一个方向输入时执行,避免对零向量求长度后进行无意义的除法。若游戏设计希望斜向移动更快,可以省略归一化;这属于玩法选择,不是语言规则。

3. 帧时间与卡顿保护

Event.ENTER_FRAME 会在影片播放过程中随帧触发。示例用计时器计算两次更新间隔,并将秒数乘以速度得到位移。帧率越高,单帧时间通常越短;帧率下降时,单帧位移会相应增加,以尽量维持单位时间内的移动距离。

代码把单帧时间限制在 0.05 秒,是为了处理切换窗口、调试暂停或运行卡顿后的大间隔。没有这个限制,恢复运行时可能出现角色瞬移。这个数值是示例中的工程取舍,不是 Flash 或 ActionScript 规定的固定上限。限制时间也意味着在严重卡顿时,角色的模拟时间会慢于真实时间;对一般角色控制通常可以接受,对要求精确模拟的系统则需要设计更完整的时间步进机制。

4. 舞台边界与注册点

在 Flash 显示列表中,player.x 和 player.y 表示显示对象注册点在父坐标系中的位置。示例假设注册点位于角色中心,于是用角色宽高的一半作为边缘留白,限制注册点范围。到达边缘时,角色被夹在边界上,不会继续移出舞台。

如果注册点在左上角,使用半宽、半高就不正确。对于简单矩形角色,可以按注册点位置计算不对称边界;对于旋转、倾斜或复杂子对象,单纯用 width 和 height 也未必对应想要的碰撞范围。需要更精确的边界时,应明确游戏采用的是轴对齐矩形、圆形还是其他碰撞形状,并按相应规则计算。

此外,本例把舞台尺寸视为移动范围。如果舞台会缩放、滚动,或角色位于嵌套容器中,那么舞台坐标和角色父容器坐标可能并不一致。此时应先统一坐标系,例如用显示对象的坐标转换方法在局部坐标与舞台坐标之间转换,而不是直接套用示例中的数值。

五、常见扩展:增加 WASD 控制

可以保留方向键,同时增加 WASD。AS3 的 Keyboard 类提供字母键常量。将下面分支加入 setKeyState 方法即可:

else if (keyCode == Keyboard.A)
{
    leftDown = isDown;
}
else if (keyCode == Keyboard.D)
{
    rightDown = isDown;
}
else if (keyCode == Keyboard.W)
{
    upDown = isDown;
}
else if (keyCode == Keyboard.S)
{
    downDown = isDown;
}

同一个方向可以由多个按键控制时,还要考虑状态覆盖问题。例如,玩家先按住左方向键,再按住 A,随后松开方向键,此时简单的布尔变量会被设为 false,即使 A 仍然按着。若游戏确实要求两套按键可以同时独立使用,需要分别记录每个键的状态,或使用按键集合,再根据集合内容计算方向。简单示例通常不必处理这类组合,但多人操作或自定义键位时应注意。

六、常见错误与排查方法

角色不移动

  • 确认 FLA 文档类型为 ActionScript 3.0,而不是 HTML5 Canvas 文档或旧版 AS2 文档。
  • 确认文档类名称与文件名一致,且源码路径可被 Animate 找到。
  • 确认实例名准确为 player,并且角色位于主时间轴上。
  • 确认舞台已获得键盘焦点。若窗口未激活,或焦点位于其他交互对象上,按键事件可能不会按预期到达。
  • 查看编译错误和输出面板。类路径、实例名、语法错误通常比移动逻辑更早导致功能失效。

角色只动一下,或移动速度不稳定

逐帧更新应读取按键状态,而不是依赖操作系统重复发送 KEY_DOWN。另外,如果使用“每帧加固定像素”的写法,帧率不同就会造成不同的实际速度。可以像示例一样按经过时间计算位移,或使用固定时间步的模拟系统。

窗口切走后角色持续移动

在失去焦点时,程序可能收不到对应的按键松开事件。示例监听 Event.DEACTIVATE 并清空按键状态,避免重新切回窗口后角色仍沿旧方向移动。具体事件行为会受运行环境影响;若应用还处理暂停、全屏切换等状态,也应在相应状态变化时清理输入。

边缘限制看起来不准

检查注册点是否位于角色中心,以及 player 是否有缩放或旋转。width 和 height 是显示对象的边界尺寸,不一定等同于美术内容的实际碰撞范围。需要可靠碰撞时,不要把视觉边界计算当成完整的碰撞系统。

重复创建后事件触发多次

如果角色或控制器会被移出舞台再加入,反复调用初始化方法可能重复添加监听器。示例在构造阶段只启动一次;如果项目需要动态创建和销毁,应在移除时取消监听器,并避免重复注册。长时间运行的项目还应管理对象生命周期,防止对象被不必要的引用保留。

七、运行与兼容边界

AS3 代码需要能执行 AS3 的运行环境。旧版 Adobe Flash Player、AIR 运行时、兼容模拟器或特定桌面播放器的支持范围并不完全相同;应按照目标环境的文档和实际测试结果判断。浏览器停止支持 Flash 插件后,不能仅靠在网页中加入 SWF 文件或修改 JavaScript 来恢复浏览器内的 Flash Player 支持。

Ruffle 等兼容项目可以运行部分 Flash 内容,但对不同 AS 版本、API 和内容功能的覆盖情况可能不同。不能据此假定本文代码在所有浏览器、移动设备或 SWF 文件中都能直接运行。维护旧项目时,建议先确认项目使用的 AS 版本、外部库、音视频接口和发布方式,再选择适合的兼容或迁移方案。

八、安全用途与限制

本文的键盘监听用于游戏输入,代码只读取按键键码并改变本地角色坐标,不涉及读取其他应用的输入、记录键盘内容、网络传输或系统级键盘监控。应将这类输入处理限制在用户正在交互的应用内,并只处理游戏需要的按键。

ActionScript 沙箱、安全策略以及运行时权限会影响文件、网络和跨域资源访问;这些规则与角色移动本身是不同问题。不能把本例解释为绕过浏览器权限或读取用户系统数据的方法,也不能因为程序监听了键盘事件,就推断它可以在任意应用或浏览器页面中捕获全局按键。面向真实用户发布旧 Flash 项目时,还需要评估运行环境的维护状态、外部资源来源及数据处理方式。

总结

控制角色移动的核心是把输入状态和位置更新分开:按键事件负责记录方向,帧更新负责根据速度和经过时间移动角色;斜向移动通过归一化避免额外加速,边界逻辑则要与注册点和坐标系保持一致。掌握这几项后,就可以在此基础上加入跳跃、加速度、动画状态切换或碰撞检测。实际应用中还要确认 AS3 运行时是否适合目标平台,不能把旧 Flash 浏览器插件视为仍受支持的部署方式。

© 版权声明

相关文章