双缓冲渲染

[原作者]:Matthew Casperson
[翻译]:小超
[翻译时间]:2010-3-6
[版权声明]:转载请保持本文完整,保留版权信息,谢谢!

在本系列的第一部分,我们写出了第一个flex应用程序类。在第二部分,我们将增加状态和双缓冲绘制工序。

Flex 的状态,这个概念很好理解:他们代表了一个程序能处于的不同状态(也就是界面  eb163.com 小超按)。例如,购物车可能有一个代表浏览网络商店的状态,还有一个代表查看某个特殊项目的状态,我们的游戏也将有许多状态,其中包括主菜单、游戏本身、角色升级时的级别总结,可能还有一个排行榜(这些部分代表着不同的功能,显示当前程序的运行状态,每个界面便是一个状态 eb163.com 小超按)。


Flex内在就支持状态。这些状态在脑海中转换,从一个图形用户界面(GUI)到另一个。他们有我们所需要的在不同状态之间转换的功能,虽然状态不必一定有任何GUI 组件。修改应用程序的当前状态的属性将触发一个状态的改变,通过给与进入状态和退出状态事件的有关函数增加必要的开始和结束代码,我们能够更新内部的游戏状态,达到匹配。

双缓冲是常用来清除绘图屏幕的视觉割裂感的相关技术。它因使用两个图像缓冲来绘制最终的图片而得名。一个缓冲在内存(后台缓存),另一个在被显示在屏幕上(前台缓冲)。你可以把后台缓冲当做一种高速寄存器,被用来绘制最终的屏幕而创建的一个单独的元素。当一个帧被画出来,它就被一个操作复制到前台缓冲。然后屏幕显示前台缓冲的内容。


Flex 有对状态的支持。状态被设计成从一个GUI(图形用户界面)向另一个GUI的在理念中的转换,他们有我们在不同状态之间转换所需要的功能,虽然状态不必一定有任何GUI 组件。修改应用程序的当前状态的属性将触发一个状态的改变,通过给与进入状态和退出状态事件有关的函数增加必要的开始和结束代码,我们能够更新内部的游戏状态以达到匹配。

双缓冲是一种用来消除因为直接画屏幕而产生的视觉割裂的常用技术。它因为使用两个图像缓冲来绘制最终的图像而得名。一个缓冲位于内存(后台缓存),另一个被显示在屏幕上的缓冲(前台缓冲)

你可以把后台缓冲当作一种高速暂存存储器,用来绘制最终的屏幕图像。当一个帧被绘制出来,它马上就被一个操作复制到前台缓冲。然后屏幕显示前台缓冲的内容。

现在,让我们看看在Fex中这些概念是如何被实现的。

main.mxml
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:Application
  3.         xmlns:mx="http://www.adobe.com/2006/mxml"
  4.         layout="absolute"
  5.         width="600"
  6.         height="400"
  7.         frameRate="100"
  8.         creationComplete="creationComplete()"
  9.         enterFrame="enterFrame(event)"
  10.         currentState="MainMenu">
  11.         
  12.         <mx:states>
  13.                 <mx:State
  14.                         name="Game"
  15.                         enterState="enterGame(event)"
  16.                 exitState="exitGame(event)">
  17.                 </mx:State>
  18.                 <mx:State name="MainMenu">
  19.                         <mx:AddChild relativeTo="{myCanvas}" position="lastChild">
  20.                                 <mx:Button x="525" y="368" label="Start" id="btnStart" click="startGameClicked(event)"/>
  21.                         </mx:AddChild>
  22.                 </mx:State>
  23.         </mx:states>
  24.         
  25.         <mx:Canvas x="0" y="0" width="100%" height="100%" id="myCanvas"/>
  26.         
  27.         <mx:Script>
  28.         <![CDATA[  
  29.         
  30.                 protected var inGame:Boolean = false;
  31.         
  32.                 public function creationComplete():void
  33.             {
  34.                         
  35.             }
  36.             
  37.             public function enterFrame(event:Event):void
  38.             {
  39.                     if (inGame)
  40.                     {
  41.                             GameObjectManager.Instance.enterFrame();
  42.                            
  43.                             myCanvas.graphics.clear();
  44.                             myCanvas.graphics.beginBitmapFill(GameObjectManager.Instance.backBuffer, null, false, false);
  45.                             myCanvas.graphics.drawRect(0, 0, this.width, this.height);
  46.                             myCanvas.graphics.endFill();
  47.                     }        
  48.             }
  49.             
  50.             protected function startGameClicked(event:Event):void
  51.                 {
  52.                         currentState = "Game"
  53.                 }            
  54.             
  55.                    protected function enterGame(event:Event):void
  56.                 {
  57.                         GameObjectManager.Instance.startup();
  58.                         inGame = true;
  59.             }
  60.             
  61.             protected function exitGame(event:Event):void
  62.                 {
  63.                         inGame = false;
  64.                 }  
  65.                
  66.         ]]>
  67.     </mx:Script>
  68.         
  69. </mx:Application>
复制代码
首先要注意的是增加了mx:Application元素的currentState(当前状态)属性。上面已经提到过,应用程序对象的currentState属性定义了程序的当前状态。通过设置此属性为MainMenu(主菜单),我们就可以说出程序从主菜单状态开始。

我们还增加了一个mx:states元素。这个元素通过子元素mx:State定义了程序能够处于的状态。我们定义了开始的两个状态:MainMenu(主菜单)和Game(游戏)。在主菜单状态,终端用户将看见游戏的开始界面,而进入游戏状态将代表游戏运行本身。


两个mx:State元素都有一个名字属性。这个属性是状态的名字,通过把Application对象的当前状态改到这个状态的名字,我们能够转换到相应的这个状态。游戏状态还包括另外两个属性:enterState(进入状态)和 exitState(退出状态)。把函数通过这些事件联系在一起,我们可以把游戏内部逻辑手动同步到这个状态。就像你看到的我们使用 EnterGame(进入游戏)这个函数来启动GameObjectManager类(稍后有此类的更多介绍),来设置内部标志inGame为true。 inGame标志为true表示在渲染循环中允许游戏绘制屏幕。ExitGame(退出游戏)函数仅仅将inGame设置为false,用于显示GUI。


还记得我曾提起在flex中状态如何被设计为脑海中的GUI转换。主菜单状态将展示这有多容易。mx:AddChild元素被用来为状态增加一个GUI元素。在这种情况下,我们增加一个按钮,游戏玩家能够点击它进入到游戏里面。而一旦我们离开了主菜单状态,flex将自动地移去这个按钮,而不需要写额外的代码。


为了让我们能够重绘屏幕我们增加了mx:Canvas(画布)元素。画布(或者更具体地说:它的graphics属性)将在双缓冲绘制过程中起到前台缓冲的作用。后台缓冲在GameObjectManager类中生成。在enterFrame函数里我们调用了GameObjectManager类的enterFrame方法,此函数让程序绘制后台缓冲。帧一被画出,我们就取后台缓冲,使用mx:Canvas元素的graphics属性的clear、beginBitmapFill、drawRect、endFill方法把生成的后台缓冲画到画布上。


  游戏对象管理器(GameObjectManager)对象负责管理组成最终游戏的各个元素,比如:敌人、玩家和各种不同的背景元素,它还负责管理画这些元素的后台缓冲。起前台缓冲是作为一个Canvas元素来实现的,这是因为一个Canvas元素能够被很容易的作为Application对象的一个子节点直接加到Application对象里。后台缓冲是用一个BitmapData对象来实现的,这使得我们能够直接快速的控制最终图像的像素。


clearColor属性指定了场景建立起来之前用来擦除后台缓冲用的颜色。最终整个后台缓冲将被游戏元素所覆盖,这个颜色将变得无关紧要,但是现在这个颜色很重要,因为它将为最终的帧建立一个很好的色块。颜色值0xFF0043AB是一种深蓝色。最前面的两位16进制值数(在0x之后)代表了alpha通道:FF 表示不透明,00表示透明。随后的六位16进制数值构成了红(00)绿(43)蓝(AB)成分。

GameObjectManager类的instance静态属性和Instance静态方法一起使用来实现单态(Singleton)设计模式。基本上我们在程序里总是只需要一个GameObjectManager对象存在(顾名思义)。通过标记GamaeObjectManager里的instance属性,,在GameObjectManager对象被创建后,我们可以保证程序中只有一个GameObjectManager对象。单态设计是一种相当常见的程序设计模式,当ActionScript缺少对私有构造器的支持,它仍然可以作为一个有用的限制多次实例化对象的工具(只要你看见一个Instance属性,极有可能对象是单态设计模式)。


lastFrame属性仅仅存储了最后一帧被绘制的时间。通过保留时间记录,我们能够判断最新的帧和当前帧的相隔时间,我们依据这个时间值来更新游戏元素。即使我们还没有任何游戏元素,我们仍然在enterFrame函数中用以为单位来计算帧之间的时间差。在startup函数内调用时,lastFrame属性中保存的时间被重置,这是因为程序不在游戏状态时GameObjectManager不能被更新。如果我们不去重置lastFrame,下一级的第一帧时间将和游戏玩家在菜单的各个级别之间花费的时间相等。游戏玩家能够在第一帧通过该级别直接结束游戏,而这可以被很好地避免。


那么现在我们完成了什么呢?通过实现状态我们生成了一个菜单屏幕,游戏玩家能够从这里通过点击按钮进入游戏。“游戏”本身只是一个双缓冲的实现,只画了一个蓝色的背景。最后通过实现状态和绘制,我们能够做到一个有趣的事情:绘制屏幕。

GameObjectManager.as
  1. package
  2. {
  3.         import mx.core.*;
  4.         import mx.collections.*;
  5.         import flash.display.*;
  6.         
  7.         public class GameObjectManager
  8.         {
  9.                 // double buffer
  10.                 public var backBuffer:BitmapData;
  11.                 // colour to use to clear backbuffer with
  12.                 public var clearColor:uint = 0xFF0043AB;
  13.                 /// static instance
  14.                 protected static var instance:GameObjectManager = null;
  15.                 // the last frame time
  16.                 protected var lastFrame:Date;
  17.                
  18.                 static public function get Instance():GameObjectManager
  19.                 {
  20.                         if ( instance == null )
  21.                         instance = new GameObjectManager();
  22.                         return instance;
  23.                 }
  24.                
  25.                 public function GameObjectManager()
  26.                 {
  27.                         if ( instance != null )
  28.                                 throw new Error( "Only one Singleton instance should be instantiated" );
  29.                                 
  30.                         backBuffer = new BitmapData(Application.application.width, Application.application.height, false);
  31.                 }
  32.                
  33.                 public function startup():void
  34.                 {
  35.                         lastFrame = new Date();
  36.                 }
  37.                
  38.                 public function shutdown():void
  39.                 {
  40.                         
  41.                 }
  42.                
  43.                 public function enterFrame():void
  44.                 {
  45.                         // Calculate the time since the last frame
  46.                         var thisFrame:Date = new Date();
  47.                         var seconds:Number = (thisFrame.getTime() - lastFrame.getTime())/1000.0;
  48.                     lastFrame = thisFrame;
  49.                     
  50.                     drawObjects();
  51.                 }
  52.                
  53.                 protected function drawObjects():void
  54.                 {
  55.                         backBuffer.fillRect(backBuffer.rect, clearColor);
  56.                 }
  57.         }
  58. }
复制代码
游戏对象管理器(GameObjectManager)对象负责管理组成最终游戏的各个元素,比如:敌人、游戏玩家和各种不同的背景元素,它还负责管理画这些元素的后台缓冲,如果你回想起前台缓冲是把它作为一个画布元素来实现的,这是因为一个画布能够被作为应用程序的一个孩子直接加到应用程序里是很方便的。后台缓冲是把它作为一个位图数据对象来实现的。这使得我们能够直接快速地复制作为最后图像的像素。

   clearColor 属性指定了图像建立起来之前用来擦除后台缓冲用的颜色。最终整个后台缓冲将被游戏元素所覆盖,和这个颜色不同,但是现在这个颜色很重要,因为它将把最终的帧涂成一整块。值0xFF0043AB 代表深蓝色。最前面的两位16进制值(在0x之后的)代表了alpha:FF 表示不透明,00表示透明。随后的六位16进制值构成了红(00)绿(43)蓝(AB)组件。

   静态instance属性和Instance函数一起使用来实现单态设计模式。基本上我们在程序里总是只需要一个GameObjectManager对象存在(顾名思义)。通过参考GamaeObjectManager里的instance 属性,我们可以总是只有一个GameObjectManager生成。单态设计是一种相当普通的程序设计模式,当actionscript 缺少对私有构造器的支持它仍然可以作为一个有用的自我测试工具(只要你看见一个Instance属性,极有可能对象是单态设计模式。

   lastFrame 属性仅仅存储了最后一帧被绘制的时间。通过保留时间轨迹,我们能够判断最后一帧和当前帧的相隔时间,这最终相反又允许我们依据这个时间来更新游戏元素。即使我们还没有任何游戏元素,我们仍然在enterFrame函数中以秒来计算帧之间的时间。当调用startup函数时,lastFrame时间被重置,这是因为程序不在游戏状态时GameObjectManager不能被更新。如果我们不重置lastFrame的值,下一级的第一帧时间将和游戏玩家在菜单的各个级别之间花费的时间相等。游戏玩家能够在第一帧通过该级别在游戏中途直接结束游戏,而这可以被很好地避免。

   那么现在我们完成了什么呢?通过实现状态我们生成了一个菜单屏幕,游戏玩家能够从这里通过点击按钮进入游戏。“游戏”本身只是一个双缓冲的实现,这里还只是画了一个蓝色的背景。最后通过实现状态和绘制,我们能够做到一个有趣的事情:绘制屏幕。
posted @ 2010-11-05 14:46  czjone  阅读(1828)  评论(0编辑  收藏  举报