ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

十年游戏编程回顾:从ActionScript 3.0到Unity的硬核基本功

十年游戏编程回顾:从ActionScript 3.0到Unity的硬核基本功 十年前我第一次打开Flash CS3在新手教程的帮助下用ActionScript 3.0写下一行trace(Hello World)的时候绝对想不到自己会在游戏编程这条路上走这么久。那会儿AS3刚普及没多久国内讨论它的社区还不算多大多数人做Flash动画还在用时间轴和AS2而我因为一本讲游戏制作的书里提到用代码给游戏写逻辑这个说法稀里糊涂就入了坑。这十年里我经历过Flash最辉煌的时期也看着它被时代推下舞台写过放在网页里的小游戏也做过需要几十个人协作的手游项目带过团队也亲手改过凌晨三点的线上Bug。回头看最想分享的不是哪个引擎多厉害、哪门语言多流行而是那些换了好几次技术栈依然管用的游戏编程基本功。这篇是上半部分主要讲我前五年的经历——从ActionScript 3.0时代切入围绕游戏循环、碰撞检测、性能优化这些绕不开的硬话题展开。如果你正在考虑进入游戏开发或者刚起步不知道从哪里下手这篇里有很多可以直接抄作业的经验。1. 起步为什么是ActionScript 3.0为什么是Flash游戏1.1 那个年代做游戏Flash几乎是唯一低门槛入口现在的人可能很难想象在智能手机还没普及的那几年想做一款能给别人玩到的游戏主流选择就那么几个用C写Windows桌面游戏用Java写手机小游戏或者用Flash做网页游戏。前两条路对新手极不友好——光是搭环境就能劝退一大半人更别提图形界面、输入处理这些琐碎的东西要自己一点点弄。而Flash不一样装上软件新建一个文档画个方块几行代码就能让它动起来浏览器一刷新就能看到效果。这种即时反馈对新手来说太重要了它让你把精力全部放在游戏逻辑上而不是被工程问题拖死。我当时选的正是ActionScript 3.0而不是当时还在大量使用的AS2。原因很简单AS3是一次彻底的重写它把Flash的脚本语言从动态脚本变成了真正的面向对象语言。类、包、强类型、事件模型、显示列表这些概念在AS3里都是正经八百的学一遍放到其他语言里照样通用。后来我做C#、做TypeScript很多思维方式都是从AS3时代带过来的。1.2 在线编程游戏和社区是我最早期的实战课堂刚开始自学那阵子光靠看书不够AS3的语法我背得下来但一到自己做游戏就卡壳。后来我发现一个特别有效的办法去玩那些在线编程游戏。当时有不少网站把编程题包装成小游戏你在网页上写AS3代码控制角色移动、躲障碍、攻击敌人代码跑对了关卡就过了。这种形式比对着书抄代码强太多了它逼着你用代码解决具体问题而且当场就能看到结果。我记得有个关卡要求你写一个跟随鼠标移动的类我反复改了七八遍才通过但那之后对鼠标事件和坐标转换的理解比看十遍教程都扎实。对一个新手来说社区的价值也绝对不能低估。当时国内有几个AS3论坛每天都有新人提问、老鸟解答我也厚着脸皮贴过自己的半成品代码求指点。有人在帖子里指出我的对象没有清理干净会导致内存泄漏那是我第一次知道removeEventListener和removeChild要成对使用。这种来自真实项目的经验是任何教程都给不了的。2. 游戏循环和显示列表AS3游戏的两根支柱2.1 帧事件驱动的游戏循环是所有游戏的心脏不管用什么语言、什么引擎游戏本质上都是一个死循环不断读取输入、更新游戏状态、渲染画面一遍一遍重复每秒几十次。AS3里这个循环是通过Event.ENTER_FRAME事件来实现的每一帧Flash都会派发这个事件你监听它在回调里写更新逻辑Flash就会在那一帧结束前把显示内容呈现在屏幕上。stage.addEventListener(Event.ENTER_FRAME, onLoop); private function onLoop(e:Event):void { // 1. 处理输入 // 2. 更新所有游戏对象的位置、状态 // 3. 检测碰撞响应事件 // 4. 其他逻辑如计分、生成敌人 }这个看起来简单的结构其实是整个游戏的中枢神经。后来我做Unity里面有Update()方法做HTML5游戏用requestAnimationFrame做小游戏平台用自己的帧回调——本质上和这里的ENTER_FRAME完全是一回事。理解了AS3的帧事件你就理解了所有游戏引擎的心跳。但这里也有一个新手最容易踩的坑不要在ENTER_FRAME回调里干太重的活。AS3是单线程的你的回调代码如果执行时间超过了帧间隔帧率就会掉。尤其是创建对象、加载资源、复杂计算这类耗时操作一定要想办法拆出去或者做缓存。我早期做的一个塔防游戏每个敌人每一帧都去new一个对象存当前状态帧率直接掉到个位数后来改成对象池后面详细讲才救回来。2.2 显示列表理解谁在屏幕上、谁不在这件事AS3的显示列表DisplayList是一个树状结构舞台是最顶层下面可以挂显示容器容器里再挂子对象。你用addChild把对象加到显示列表上它才可见用removeChild把它移走它才从屏幕上消失。听起来很简单但很多人写久了就会出问题——对象忘了移除导致不可见的对象还在参与渲染、还在执行逻辑游戏越来越卡。我当时自己写过一个养成类小游戏玩家每点一次按钮就生成一个漂浮的加分数字飞向记分板。功能是做好了但玩家玩十分钟之后游戏明显变卡。最后排查发现那些加分数字飞完之后我并没有把它们从显示列表移除几百个对象堆在那里每帧都在更新位置。修复方法就是在数字飞行动画结束后立即removeChild并置空引用。这一点后来我写任何项目都会特别注意显示对象离开屏幕不是终点从显示列表移除才是。还有内存管理的问题。AS3有垃圾回收机制但它的回收时机不可控而且有一个很坑的特性如果对象被一个长期存活的对象强引用着垃圾回收就永远不会处理它。所以我们在项目里约定凡是监听全局对象比如stage事件的地方一定要在不需要时手动removeEventListener必要的时候用弱引用stage.addEventListener(Event.ENTER_FRAME, onLoop, false, 0, true);最后一个参数是useWeakReference设为true表示弱引用这样即使忘记移除监听对象被置空以后垃圾回收也能把它收走。虽然不能完全依赖弱引用偷懒但它确实是降低内存泄漏概率的有效手段。3. 从能动到好玩写游戏逻辑时真正要琢磨的事3.1 碰撞检测的三板斧什么时候用API什么时候自己算AS3自带的碰撞检测只有两个hitTestObject基于对象矩形边界和hitTestPoint基于点坐标。它们应对简单场景很省事比如判断子弹有没有打中敌人直接bullethitTestObject(enemy)就行。但它们有个致命问题矩形碰撞对不规则形状完全不准确。一个圆形的敌人你用一个矩形框去判断明明离得很远就因为矩形的角碰到了就算命中了玩家会觉得这判定也太离谱了吧。所以实际做游戏碰撞检测大多要自己写。常见的方案有三种方案原理适用场景性能AABB包围盒两个轴对齐矩形是否有重叠敌人、建筑、道具等规则物体极快适合大量对象圆形碰撞两圆心距离是否小于半径之和子弹、角色、圆形敌人快精度比矩形好像素级检测逐像素比对透明度精确判定如特效、地图碰撞很慢尽量少用圆形碰撞是我个人最常用的一种因为它够快又够准。判断两个圆的碰撞只需要算一次距离private function checkCircleCollision( ax:Number, ay:Number, aRadius:Number, bx:Number, by:Number, bRadius:Number):Boolean { var dx:Number ax - bx; var dy:Number ay - by; var distSq:Number dx * dx dy * dy; var minDist:Number aRadius bRadius; return distSq minDist * minDist; }注意这里我用的是距离平方而不是直接开根号因为开根号运算比乘法慢。在每帧要检测几十上百对物体的时候这个优化能省下不少CPU。这种少用Math.sqrt的习惯到后来做移动端游戏时帮了我大忙移动设备CPU更弱一点一滴的性能优化都是实打实的帧率。3.2 手感调优游戏编程里最容易被低估的部分很多新手写完碰撞、写完整套逻辑发现游戏能玩了但玩起来就是别扭。角色移动僵硬、攻击没有打击感、子弹速度忽快忽慢——这些都不是Bug而是手感问题。手感这个东西说起来玄拆开看其实就是一堆数值和规则在起作用。举个最典型的例子角色移动。最笨的写法是每帧设置固定速度player.x 5;这样写的问题在于如果帧率不稳定玩家在60帧的电脑上角色每秒移动300像素在30帧的低配电脑上就只有150像素游戏体验完全不一样。正确做法是乘以时间增量让移动速度与帧率无关player.x speed * dt; // dt是这一帧过去的时间单位秒其中speed的单位是像素/秒dt由帧间隔计算得出。这样无论帧率高低角色每秒钟的实际移动距离都一致。这个基于时间的运动的概念在AS3里需要自己用getTimer()算到了Unity里就有了现成的Time.deltaTime。明白了底层原理换引擎只是换个API的问题。手感调优的另一个点是攻击反馈。我做那款塔防游戏时一开始子弹打到敌人身上敌人只是掉血没有任何视觉反馈测试完大家都说打起来跟假的一样。后来我加了三样东西命中瞬间敌人闪白一下、子弹命中时产生一个小粒子特效、敌人头顶飘出伤害数字。代码量不大但游戏立刻活了。做游戏编程永远要记住画面和代码是两条腿只写逻辑不管表现游戏走不远。4. 性能就是生命Flash游戏的优化实战4.1 对象池别让游戏在创建和销毁中卡死做游戏不可避免地要频繁生成和销毁对象最常见的就是子弹和敌人。如果每次发射子弹都new一次子弹出屏就销毁在子弹数量少的时候没感觉一旦满屏弹幕每一帧都有大量对象的创建和销毁Flash的垃圾回收就会被频繁触发帧率就会像过山车一样掉。解决方案就是对象池。简单说就是预先创建一批对象放在池子里要用的时候从池子里取一个用完再还回去不真正销毁public class BulletPool { private var pool:Array []; private var active:Array []; public function getBullet():Bullet { var bullet:Bullet; if (pool.length 0) { bullet pool.pop(); } else { bullet new Bullet(); } active.push(bullet); return bullet; } public function recycleBullet(bullet:Bullet):void { bullet.reset(); // 从active里移除放回pool // 同时要从显示列表移除但对象本身保留 } }对象池的好处是双重的一方面避免了频繁new带来的性能损耗另一方面因为对象复用内存占用是稳定的不会忽高忽低。后来做Unity项目时我第一时间就把对象池这个模式移植了过去C#版的写法几乎一模一样。可以说对象池是游戏编程里最值得掌握的性能优化手段没有之一。4.2 渲染瓶颈为什么你的游戏在看不见的地方变慢Flash的渲染有两个大坑一个是滤镜filter一个是位图的实时缩放。那时候大家很喜欢给角色加一个发光滤镜让它看起来更炫但滤镜会触发Flash对显示对象做额外的像素处理这种处理非常消耗性能。尤其是滤镜作用在有大面积透明区域的对象上时Flash不得不计算整个矩形区域的所有像素代价极高。我的经验是能用美术素材直接做出来的效果就不要用代码滤镜非用不可时尽量把滤镜缓存成位图cacheAsBitmap true避免每帧重复计算。还有一个更容易被忽略的问题你不在屏幕上的对象也在被渲染。Flash对可见区域外的对象的处理机制比较微妙但如果你把几百个对象放在舞台范围外还持续更新它们帧率照样会受影响。所以后来我们团队的规范是离屏对象要么暂停更新要么干脆从显示列表移除。这个习惯看起来简单但对游戏流畅度的提升非常大。性能问题的排查方法也要讲一下。AS3时代没有现在这么好用的Profiler我用的土办法是二分注释法先把一半逻辑注释掉跑一遍看帧率有没有变化如果没有问题就在另一半逻辑里然后继续二分直到定位到具体对象或方法。这个办法土但非常有效而且培养了我对性能瓶颈的敏感度——一旦游戏卡顿我会先从每帧在跑什么入手梳理而不是瞎猜。到后来用Unity的Profiler我能一眼看出哪段代码是热路径全靠AS3时代练出来的直觉。5. 转型抉择Flash退场之后那些老本事为什么还能打5.1 我做了哪些技术栈迁移踩过哪些坑大概在2013年前后Flash衰落的迹象越来越明显。HTML5技术在游戏领域的表现越来越好移动端的手游市场也爆发了单纯的Flash网页游戏开始走下坡路。我那时候面临一个选择是继续守着Flash还是往新的方向走。我的选择是两条腿走路一边用TypeScript和Canvas做HTML5游戏一边开始学Unity做手游。事实证明这个选择帮我平稳渡过了行业转型期。但转型的过程并不轻松最大的障碍不是语法而是思维方式的转换。比如Unity的组件式架构一个GameObject挂多个Component和AS3的继承式架构所有东西都是Sprite的子类是完全不同的组织逻辑。我从AS3带过来的万物皆类的惯性思维在Unity里吃了不少苦头改了很久才习惯。不过我也很快发现游戏循环、状态机、对象池、碰撞检测、资源管理、事件监听这些核心概念在哪个平台都一模一样。C#的Update()就是AS3的ENTER_FRAMEUnity的Prefab就是Flash里的MovieClip资源复用Unity的AddComponent和AS3的addChild本质都是在往一个会跑的对象身上挂东西。语言只是外衣游戏编程的内核从未变过。5.2 十年后回头看真正值钱的是哪些基础能力这几年我面试过不少新人也带过好几个实习生。我发现一个现象学历背景好、工具用得溜的人不少但真正能把一款游戏从零做到完整的人不多。原因就在于很多人只学会了操作引擎没有理解游戏编程的底层逻辑。在我看来游戏编程真正值钱的基础能力就四块一是游戏循环的掌控力知道每一帧发生了什么、什么该在什么时候执行二是数学功底向量、坐标系变换、插值这些在游戏里无处不在三是内存和性能意识知道什么操作开销大、怎么避免浪费四是调试能力能快速定位为什么这里少了一个对象。这四块能力我在AS3时代就打下了基础后来不管换什么引擎都是在这四块地基上添砖加瓦。基础能力AS3时代的体现后来的应用场景游戏循环Event.ENTER_FRAMEUnity Update、浏览器requestAnimationFrame数学功底圆形碰撞、向量移动3D空间计算、寻路算法、物理模拟性能意识对象池、离屏对象清理移动端内存优化、GC压力控制调试能力二分注释法定位卡顿Profiler热路径分析、崩溃排查所以如果有人问我十年前从AS3开始学游戏编程是不是走了弯路我会说完全不是。每一种技术都有它的生命周期但你在这段生命周期里积累的底层能力是跟着你走的。5.3 十年经验浓缩成的常见问题速查表最后把我这些年最常遇到的问题和对应的解决思路整理成一张表这些问题的答案在AS3时代适用换到其他引擎也同样成立现象真正原因解决思路游戏越玩越卡显示对象没有移除或者监听器没有注销检查对象生命周期离屏即移除removeEventListener和removeChild成对使用帧率忽高忽低每帧创建销毁大量对象触发频繁GC引入对象池复用对象避免高频率new角色在不同电脑上速度不一致移动逻辑直接用固定像素值没有乘时间增量改用基于时间的运动速度乘以帧间隔碰撞不真实用了矩形碰撞检测处理不规则物体改用圆形碰撞或自定义数学判断发光的特效让游戏掉帧滤镜每帧实时计算像素用cacheAsBitmap缓存或者直接用美术素材替代子弹打中敌人没有反馈只有数值变化没有视觉表现加受击闪白、粒子特效、飘字反馈这张表里的每一个问题我都一对一地踩过坑、修过线上版本、被玩家骂过。做游戏编程就是这样很多道理看起来简单但不亲手栽一次跟头你是记不住的。我个人在写了十年游戏代码之后最大的体会是游戏编程是一门慢功夫学科你学到的每一个基础概念都会在未来的某个项目里以你想不到的方式回报你。AS3那个时代已经过去了但我在那个时代练出来的手艺到现在还在用。这篇先讲到这里后面有机会再聊聊我做手游之后的故事——那又是另一场完全不同的战斗了。
返回列表