
Flash游戏编程基础教程:用 ActionScript 控制角色移动
角色移动是许多游戏的基础功能:玩家按下方向键,角色便在舞台上移动;松开按键,角色停止。这个看似简单的功能,实际牵涉输入事件、帧更新、坐标系、速度计算和边界处理。把这些基础概念处理好,后续添加动画、碰撞检测或关卡系统时会更容易。
本文使用 ActionScript 3.0(AS3),以 Adobe Animate 中的 Flash 文档为例,完成一个可以运行的键盘移动示例。需要先说明运行环境的现实情况:Adobe Flash Player 已于 2020 年底停止支持,主流浏览器也不再运行其插件。因此,本文代码适合维护旧项目、学习 AS3、在兼容的离线环境中测试,或作为移植到其他运行时的基础;它不是让现代浏览器重新启用 Flash Player 的方法。
一、适用场景与实现思路
AS3 的键盘移动适用于仍需维护的 Flash 游戏、交互课件和离线演示,也适合学习事件驱动编程。典型场景是:用方向键或 WASD 控制一个显示对象在二维舞台上移动。
实现时需要区分两个时间点:
- 按键事件:键盘按下或松开时,更新按键状态。
- 帧更新事件:每帧根据当前按键状态计算位移,并更新角色坐标。
不建议只在 KEY_DOWN 事件中直接移动角色。按住一个键时,操作系统可能以一定频率重复发送按键事件;这个频率会因系统设置和运行环境而异,角色速度也就不容易保持一致。更可靠的做法是记录哪些键正处于按下状态,然后在帧更新中统一移动。
二、准备 Animate 文档
- 新建一个 ActionScript 3.0 文档,并保存为
MovementDemo.fla。 - 在舞台上放置一个角色图形,将其转换为影片剪辑(MovieClip)。
- 选中影片剪辑,在“属性”面板的实例名称栏中设置实例名为
player。实例名区分大小写。 - 建议将角色的注册点放在图形中心,便于理解后文的边界计算。注册点不在中心也可以运行,但坐标含义和边界策略要相应调整。
- 在文档属性中设置文档类为
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 浏览器插件视为仍受支持的部署方式。









