最近剛好有做2D遊戲的點光源效果,然後就擴展一下,研究了一下戰爭迷霧的效果。主要是想實現類似魔獸爭霸那種人物走動,然後黑色的戰爭迷霧隨着人物的移動漸漸打開的效果。使用具有漸變透明圖片作爲光源來使得戰爭迷霧呈現出平滑的效果。本文後面介紹了兩個簡單的實現方法,效果有細微的差別,有興趣的同學可以分別研究。最後也有完整展示代碼和提供例子下載。
一、常見的戰爭迷霧效果
早期的紅警的戰爭迷霧大家應該也比較熟悉,不過看起來沒那麼平滑,應該是採用圖塊拼出來。可以明顯看得出一些方方塊塊。
可見早期魔獸爭霸2也是沒那麼平滑的。
後面出的一些有些的戰爭迷霧就會有比較平滑的過渡效果了。比如英雄聯盟,魔獸3(這兩個是3D的)。但是也有2D也做得比較平滑(看圖的最後一個截圖)
二、實現的簡單原理講解
由於是做2D遊戲,所以思路基本上是2D,但是同時也有3D思想,例如圖層混合方式處理等。主要原理就是根據圖片的alpha值來進行反向擦除(alpha爲1時完全擦除,alpha越小則越不透明,實現漸變過程效果)。最後用了兩種比較簡單的方法來實現了這種效果,兩者有微弱的差別。這裏採用的是AS3實現的,源碼也提供了幾種漸變圖片,可以作爲點光源來擦亮迷霧,可以替換上去看各種效果的。
- 遮罩擦除做法
最先想到的原理,是基於之前實現類似點光源效果的做法。通過一層帶有alpha值遮罩圖來擦掉對應的戰爭迷霧,就是移動版增大的點光源效果。首先人物背景,然後一層戰爭迷霧在最頂層,人物帶了個點光源,然後人物移動的時候,不會把那個點光源層進行繪製,那麼光源層就會原來越大。那個迷霧自然就會越來越大了。
下圖:剛開始是一個圓圈,然後隨着人物移動,圓圈會擴大。
得到這個圖之後,就是對黑色戰爭迷霧層根據alpha進行擦了。
//一個專門做點光源的頂層容器
var topContainer:Sprite = new Sprite();
//強制爲該顯示對象創建一個透明度組
topContainer.blendMode = BlendMode.LAYER;
//根據顯示對象的 Alpha 值擦除迷霧
openFogBitmap.blendMode = BlendMode.ERASE;
合成遮罩圖來去除迷霧的代碼是,最後一個參數true是合併alpha:
bitmapData.copyPixels(bitmapData,pointRect,new Point(role.x,role.y),null,null,true);
這樣跟隨人物移動不斷地把遮罩擴大。除了最開始的合成遮罩圖,後面的處理跟之前講的新手引導遮罩和點光源實現機制一樣,後面會給出相關的代碼。不過這種實現是有點不好的是合併alpha,這樣會導致範圍突然變亮(因爲alpha相加大於1,就全部擦了,大部分亮了,也就是會有個逐漸變亮的效果,使得戰爭迷霧開啓效果沒那麼真實)。最終表現效果如下圖:
2. 直接擦出戰爭迷霧方法(橡皮擦功能)
實際測試了下,對遮罩擦除做法這個效果不太滿意。於是再研究了一下,想到了橡皮效果,直接用點光源圖片把戰爭迷霧一點點擦掉又如何呢?趕緊仔細看了相關api,還真有類似的實現效果。主要還是bitmapData的draw方法,重點是這個方法的第四個參數,
source:IBitmapDrawable — 要繪製到 BitmapData 對象的顯示對象或 BitmapData 對象
matrix:Matrix (default = null) — 一個 Matrix 對象,用於縮放、旋轉位圖或轉換位圖的座標。。
colorTransform:flash.geom:ColorTransform (default = null) — 一個 ColorTransform 對象
blendMode:String (default = null) — 指定要應用於所生成位圖的混合模式。
所以我們每次在戰爭迷霧這個層次這裏每次根據玩家移動,調用draw方法把角色帶的點光源圖片給draw進入戰爭迷霧的BitmapData中,然後設置爲根據alpha的參數來擦出,露出最終的背景就行了。
BlendMode.ERASE //提供混合模式可視效果的常量值的類。
//設置需要draw的座標位置
var matrix:Matrix = new Matrix(1,0,0,1,role.x,role.y);
fogBitmapData.draw(pointBitmap,matrix,null,BlendMode.ERASE);
最終效果圖:
代碼實現
代碼已經有比較詳細的註釋了,這裏不做解釋,具體自己看代碼了。可以運行兩個例子來比較。
代碼例子源碼下載:2D遊戲戰爭迷霧的實現例子(AS3版本)
1. 遮罩擦除做法代碼,FogLightTest.as
/**
* 戰爭迷霧遮罩燈效果測試例子
* @author sodaChen
* Date:2017-2-16
*/
[SWF(width="1274",height="768")]
public class FogLightTest extends Sprite
{
/** 背景 **/
[Embed(source = "res/alpha/bg.jpg")]
private var bgClass:Class;
//點光源圖片
[Embed(source = "res/alpha/light4.png")]
private var shadowClass:Class;
/** 打開的迷霧圖像源,用來擦出迷霧 **/
private var openFogBitmap:Bitmap;
/** 原始點光源圖片,用來確定一個角色視野範圍的 **/
private var pointBitmap:Bitmap;
/** 存放已經開啓的迷霧圖片 **/
private var pointBitmapDatas:Vector.<uint>;
/** 點光源的大小範圍 **/
private var pointRect:Rectangle;
private var role:Sprite;
/** 光源的移動 **/
private var speed:int = 10;
public function FogLightTest()
{
addEventListener(Event.ADDED_TO_STAGE,onStage);
}
private function onStage(evt:Event):void
{
//添加背景
addChild(new bgClass());
/////////////////////////////文本的正式測試代碼啦/////////////////////////////
//新建一個專門做點光源的頂層容器
var topContainer:Sprite = new Sprite();
topContainer.mouseEnabled = false;
//強制爲該顯示對象創建一個透明度組
topContainer.blendMode = BlendMode.LAYER;
addChild(topContainer);
//創建黑色的迷霧
var mask:Shape = new Shape();
mask.graphics.beginFill(0x000000);
mask.graphics.drawRect(0,0,1274,768);
mask.graphics.endFill();
topContainer.addChild(mask);
//製作點光源,用來擦亮迷霧,具體的擦出在下面的鼠標事件那裏
pointBitmap = new shadowClass();
//創建擦亮迷霧後的視野圖片
pointRect = new Rectangle(0,0,pointBitmap.bitmapData.width,pointBitmap.bitmapData.height);
openFogBitmap = new Bitmap(new BitmapData(1274,768,true,0));
//複製最開始的位置
openFogBitmap.bitmapData.copyPixels(pointBitmap.bitmapData,pointRect,new Point(450,300));
//根據顯示對象的 Alpha 值擦除迷霧
openFogBitmap.blendMode = BlendMode.ERASE;
topContainer.addChild(openFogBitmap);
/** 移動中的主角 **/
role = new Sprite();
role.x = 450; role.y = 300;
topContainer.addChild(role);
stage.addEventListener(KeyboardEvent.KEY_DOWN,onKeyDown);
}
private function onKeyDown(evt:KeyboardEvent):void
{
//4個方向鍵的控制
if(evt.keyCode == Keyboard.DOWN) role.y += speed;
else if(evt.keyCode == Keyboard.UP) role.y -= speed;
else if(evt.keyCode == Keyboard.LEFT) role.x -= speed;
else if(evt.keyCode == Keyboard.RIGHT) role.x += speed;
//暫時不考慮性能,不停地寫新的圖像數據
openFogBitmap.bitmapData.copyPixels(pointBitmap.bitmapData,pointRect,new Point(role.x,role.y),null,null,true);
}
- 直接擦出戰爭迷霧方法(橡皮擦功能)代碼,EraserFogTest.as
/**
* 迷霧戰爭擦除效果
* @author sodaChen
* Date:2017-2-16
*/
[SWF(width="1274",height="768")]
public class EraserFogTest extends Sprite
{
/** 背景 **/
[Embed(source = "res/alpha/bg.jpg")]
private var bgClass:Class;
//點光源圖片
[Embed(source = "res/alpha/light4.png")]
private var shadowClass:Class;
/** 打開的迷霧圖像源,用來擦出迷霧 **/
private var openFogBitmap:Bitmap;
/** 光源,擦亮迷霧的範圍 **/
private var pointBitmap:Bitmap;
private var role:Sprite;
/** 光源的移動 **/
private var speed:int = 10;
/** 迷霧的圖像數據源 **/
private var fogBitmapData:BitmapData;
public function EraserFogTest()
{
addEventListener(Event.ADDED_TO_STAGE,onStage);
}
private function onStage(evt:Event):void
{
//添加背景
addChild(new bgClass());
/////////////////////////////文本的正式測試代碼啦/////////////////////////////
//新建一個專門做點光源的頂層容器
var topContainer:Sprite = new Sprite();
topContainer.mouseEnabled = false;
//強制爲該顯示對象創建一個透明度組
topContainer.blendMode = BlendMode.LAYER;
addChild(topContainer);
//創建黑色的迷霧
var mask:Shape = new Shape();
//顏色可以選自己喜歡的
mask.graphics.beginFill(0x000000);
mask.graphics.drawRect(0,0,1274,768);
mask.graphics.endFill();
fogBitmapData = new BitmapData(1274,768);
fogBitmapData.draw(mask);
topContainer.addChild(new Bitmap(fogBitmapData));
//製作點光源,用來擦亮迷霧,具體的擦出在下面的鼠標事件那裏
pointBitmap = new shadowClass();
/** 移動中的主角 **/
role = new Sprite();
role.x = 450; role.y = 300;
topContainer.addChild(role);
//默認位置
openFog();
stage.addEventListener(KeyboardEvent.KEY_DOWN,onKeyDown);
}
private function onKeyDown(evt:KeyboardEvent):void
{
//4個方向鍵的控制
if(evt.keyCode == Keyboard.DOWN) role.y += speed;
else if(evt.keyCode == Keyboard.UP) role.y -= speed;
else if(evt.keyCode == Keyboard.LEFT) role.x -= speed;
else if(evt.keyCode == Keyboard.RIGHT) role.x += speed;
openFog();
}
private function openFog():void
{
//設置需要draw的座標位置
var matrix:Matrix = new Matrix(1,0,0,1,role.x,role.y);
//正常的daw方法,主要參數是後面的BlendMode.ERASE。根據pointBitmap的透明度來擦除fogBitmapData
fogBitmapData.draw(pointBitmap,matrix,null,BlendMode.ERASE);
}