列位仙家……许久不见,现在咱啥也别说了,眼泪唰啦唰啦地……不,哥没哭,哥这是高兴……(旁白:“高兴你个头……兴你个头……你个头……个头……头……”) 我*,你***掺和什么掺和,哥跟仙家们见面关你*事,你再****我就*****,我*,怎么说出来的话都是星号?!还伴随着一声清脆的“滴……”声?!(旁白:“神经病……”)切~说我发神经就是你没幽默感的表现了…… 算了,不扯了,伤和气,这和气一伤,啪~容易扯着蛋~!相信列位仙家都看过我的 案例5——视角移动 吧?那时讲了一些关于视角移动的原理,当时提到视角的移动时列位不知会不会联想到一些大型游戏的地图或者是一些常用的地图应用呢?不管你们想没想到,反正寡人是想到了,既然想到了,就当是拓展训练,为列位揭秘flash地图应用的制作方法吧。 flash地图应用大致可分两分静态地图和动态地图两类,所谓静态地图就是地图始终是一张固定的图片,此图片会很大,长宽都可能是舞台的数倍,而动态地图就是一次只初始化用户可视范围内的地图数据,当拖动地图时会向服务器发起请求拿到新的一块地图区域的信息然后再渲染出来,像百度,谷歌,E都市的地图均是如此。 我们今天先讲静态地图。看看我静态地图的最终结果先吧: http://www.iamsevent.com/upload/ASMap.swf ) 它的制作要点主要分两点:一,拖拽移动视角;二,地图缩放。第一点对于列位来说应该没有什么问题,若不明白再去翻翻案例五吧老湿……至于第二点呢,简单地说就是图片坐标以及scaleX,scaleY的改变,难点就在于保持以当前视野中心点为中心等比缩放上面。我们知道,当你对一个Sprite或是其他显示对象通过改变scaleX,scaleY进行缩放的时候,显示对象会以注册点(坐标零点)为中心进行缩放,那么当你把一张图片addChild到一个Sprite中后,改变此Sprite的scaleX,scaleY进行缩放时会发现图片会向右下角延伸,要想让图片等比缩放必须把图片中心点放在Sprite原点才行。不信你试试? 测试过之后你一定能体会到,只有注册点在中心时才可能做到在改变scale时等比缩放。不过我们的地图在按下鼠标后会被拖动,不停地在改变其坐标位置,所以你一开始把注册点设置在地图图片中心位置是不够的,注册点还得跟着鼠标的拖动而不停地改变。有人就问了,啊?注册点也能动态改变吗?额~其实这个“注册点”得加引号啦,其实质还是x,y的变化,让我们看看它的原理所在。 首先,我们一个地图对象(可以为bitmap也可以把bitmap放至一个Sprite中)的默认原点位置 (0,0) 依然在左上角,若不更改地图addChild时的位置为负的一半地图长、宽,则对地图进行缩放时地图将会向右下方扩散。我们看下图 <ignore_js_op> 假设黑色区域为地图的父容器(可以看作是舞台,这也是我们用户的可视区域),白色十字为原注册点,在父容器中心,当淡蓝色的原地图放大时,我们能够想象注册点将会向右下方跑。此时用户会明显地看到刚才父容器中心点位置往右移动了,这自然不能达到我们想要的目的,我们是希望用户视野中心点位置保持不变然后等比缩放它的四周区域。那怎么办呢?自然是把随着scale的放大而往右下方偏移的注册点移动回来。那么到底需要移动多少呢? 从图上我们其实可以看出,注册点在地图放大过程中右移的位置为它缩放前后与父容器(黑色区域)左边缘的距离之差,同理,我们也可以得到下移了的位置: offsetX = X0 - X0 * scaleX; offsetY = Y0 - Y0 * scaleY; 上面的公式中的X0,Y0是注册点相对于父容器的横纵坐标,而不是相对于地图本身,因为地图本身会进行缩放,在不停地变动,那么就不能以它自身作为参照物,就像你想测一测你跑步有多快,你肯定是以一样静止的物体,比如一棵大树为参照物,不可能以一个运动的物体,如一个和你并行奔跑的人为参照物,若你以运动的物体作为参照物,那你只会感觉你跑的很慢甚至没有移动过,但实际上你已经跑得跟狗一样快了。要计算地图相对于父容器的横纵坐标,使用localToGlobal以及globalToLocal方法是再简单不过的了: var A : Point=mc.parent.globalToLocal(mc.localToGlobal(regpoint)) //先把注册点转换为全局坐标再转换为它父容器的坐标 mc.scaleX = mc.scaleY = value; //执行放大后,再重新计算全局坐标 var B : Point=mc.parent.globalToLocal(mc.localToGlobal(regpoint)) var offsetX:Number = A.x - B.x; var offsetY:Number = A.y - B.y; 如此便可求得我们注册点在放大/缩小后移动了的距离。为了让缩放后改变了的注册点回到父容器中心,我们把地图的横纵坐标进行如下调整: mc.x+=offsetX; mc.y+=offsetY 验证一下有没有错误。若地图放大,则offsetX, offsetY将是负值,若缩小,则为正值,地图放大后注册点往右下方偏离(见上图所示),所以为了让注册点回到父容器中心,需把地图往左上移动,那么让地图的横纵坐标加上一个负值就等于让地图的横纵坐标进行了减小,确实能够使地图往左上移动。OK,证明这样做没错后就可以把这个原理用到我们的实际开发中去了。 需要注意的是,我们的这个所谓的“注册点”不过只是一个参照点而已,它的用途就是在缩放前后计算用户所关注的点偏移了的位置,然后用这个位置来让地图x,y进行移动以保证用户关注点保持在原来的“位置”。不过如果我们要对地图进行拖动的话就必须改变地图的x,y坐标,当地图坐标改变时我们的“注册点”位置也必须跟着移动以保证它在地图中所处的位置不变才行。不然当你拖动了地图,注册点还留在原地的话它在地图中所处位置就会变掉了,缩放后计算出来的偏移位置就会出现错误。看看源码: 假设我们在水平以及竖直方向拖动地图的距离分别为distX和distY,那么拖动后地图坐标为 map.x += distX; map.y += distY; 假设我们起初设置的地图位置为 map.x = 0; map.y = 0; 注册点位置为舞台中心点regPoint = new Point(stage.stageWidth/2, stage.stageHeight/2); 那么移动了地图后它的位置依然要和map的左上角保持距离: regpoint.x = -map.x + stage.stageWidth/2; regpoint.y = -map.y + stage.stageHeight/2; 若地图进行过缩放,那么就需要把地图坐标换算为1倍大小时候的坐标再进行计算: regpoint.x = -map.x / map.scaleX + stage.stageWidth/2; regpoint.y = -map.y / map.scaleY + stage.stageHeight/2; 切记,每当地图坐标改变时,注册点坐标也得跟着改变。 阅读完了理论知识的一些要点之后看看全部源码,其中还不乏一些需要注意的地方:
[SWF(width="600", height="500")]
public class ASMap2 extends Sprite
{
[Embed(source="assets/wowmap.jpg")]
private var MapResource:Class;
private var map:Sprite = new Sprite();//地图
private var oldX:Number;//拖动前X
private var oldY:Number;//拖动后Y
private var currentMapZoom:Number = 1;//当前地图缩放比例
private var maxZoomLevel:int = 5;//最大可放大次数
private var minZoomLevel:int = -3;//最大可缩小次数
private var currentZoomLevel:int = 1;//当前缩放次数
private var zoomValue:Number = 0.1;//每次放大或缩小的比例
private var bmp:Bitmap;
private var regpoint:Point//地图注册点
public function ASMap2()
{
initView();
initBtn();
}
private function initView():void{
bmp = new MapResource();
map.addChild( bmp );
addChild( map );
map.x = 0;//地图真实注册点在0,0位置
map.y = 0;
regpoint = new Point( stage.stageWidth/2, stage.stageHeight/2 );//虚拟注册点位置为舞台中心
map.doubleClickEnabled = true;//设置地图容器能接受双击事件
map.addEventListener( MouseEvent.MOUSE_DOWN, onMouseDown );
map.addEventListener( MouseEvent.MOUSE_UP, onMouseUp );
map.addEventListener( MouseEvent.DOUBLE_CLICK, onDoubleClick );
map.cacheAsBitmap = true;//缓存为bitmap,提高拖动效率
}
private function initBtn():void{
var zoomIn:MyButton = new MyButton("放大", 50, 25);
var zoomOut:MyButton = new MyButton("缩小", 50, 25);
zoomIn.x = 240;
zoomOut.x = 300;
zoomIn.y = zoomOut.y = 15;
addChild( zoomIn );
addChild( zoomOut );
zoomIn.addEventListener(MouseEvent.CLICK, zoomInHandler);
zoomOut.addEventListener(MouseEvent.CLICK, zoomOutHandler);
}
/**
* 设置镜头
*
*/
private function setCamera( x:Number, y:Number ):void{
var desX:Number = map.x + stage.stageWidth / 2 - x;
var desY:Number = map.y + stage.stageHeight / 2 - y;
//使用TweenLite缓动改变地图x,y值时,x,y是缓缓改变而不是立马改变的(切记),所以需要在缓动结束后再改变注册点
TweenLite.to( map, 0.6, {x:Math.min( Math.max(desX, stage.stageWidth - map.width), 0 ),
y:Math.min( Math.max(desY, stage.stageHeight - map.height), 0 ),
onComplete:offsetRegpoint} );
// offsetRegpoint(); //注意不能在这里改变注册点,因为此时map的x,y还未变到预期值
}
private function onMouseDown( event:MouseEvent ):void{
addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
stage.addEventListener(Event.MOUSE_LEAVE, onMouseUp);//防止按住鼠标拖动过程中鼠标滑出flash player窗口后再回来造成的鼠标跟随现象
oldX = stage.mouseX;
oldY = stage.mouseY;
}
private function onMouseMove( event:MouseEvent ):void{
//地图拖动(视角移动)
var distX:Number = stage.mouseX - oldX;
var distY:Number = stage.mouseY - oldY;
map.x += distX;
map.y += distY;
//限制地图移动范围,防止露出舞台背景
map.x = Math.min(0, map.x);
map.x = Math.max((stage.stageWidth - map.width), map.x);
map.y = Math.min(0, map.y);
map.y = Math.max((stage.stageHeight - map.height), map.y);
//地图位置一变,注册点必须跟着变
offsetRegpoint();
oldX = stage.mouseX;
oldY = stage.mouseY;
}
private function onMouseUp( event:Event ):void{
removeEventListener( MouseEvent.MOUSE_MOVE, onMouseMove );
}
private function onDoubleClick( event:MouseEvent ):void{
setCamera( event.stageX, event.stageY );
}
private function zoomInHandler(event:MouseEvent):void{
if( currentZoomLevel + 1 <= maxZoomLevel ){
zoom( 0 );
}
}
private function zoomOutHandler(event:MouseEvent):void{
if( currentZoomLevel - 1 >= minZoomLevel ){
zoom( 1 );
}
}
/**
* 根据地图位置移动注册点
*
*/
private function offsetRegpoint():void{
regpoint.x = -map.x / map.scaleX + stage.stageWidth/2;
regpoint.y = -map.y / map.scaleY + stage.stageHeight/2;
}
/**
* 缩放
* @param action 操作方式:0为放大,1为缩小
*
*/
private function zoom( action:int ):void{
if( action == 0 ){
currentZoomLevel++;
currentMapZoom += zoomValue;//缩放比例加zoomValue
}else if( action == 1 ){
currentZoomLevel--;
currentMapZoom -= zoomValue;//缩放比例减zoomValue
}
var A:Point = this.globalToLocal( map.localToGlobal(regpoint) );
var temp:Number = map.scaleX;//临时记录scale值,由于scaleX和scaleY值一样,所以我就随便取一个赋值
map.scaleX = map.scaleY = currentMapZoom;//暂时缩放以计算缩放后的注册点位置以及地图宽高
//执行放大,再重新计算全局坐标,重新记录宽高
var B:Point=this.globalToLocal( map.localToGlobal(regpoint) );
var newW:Number = map.width;
var newH:Number = map.height;
//把注册点从B点移到A点
//计算缩放前后舞台中心点所对应地图位置所移动的距离
var offsetX:Number = A.x-B.x;
var offsetY:Number = A.y-B.y;
//将缩放前视野中心点对应地区位置移回舞台中心,让人感觉到地图是等比缩放了的
//当然,你还得限制住map不要移动太多以致露出舞台背景,最小值需用放大后的长宽进行计算
var newX:Number = Math.min( Math.max(map.x + offsetX, stage.stageWidth - newW), 0 );
var newY:Number = Math.min( Math.max(map.y + offsetY, stage.stageHeight - newH), 0 );
map.scaleX = map.scaleY = temp;//恢复到原来缩放大小
//播放舞台缩放动画,这才是真正的缩放开始
TweenLite.to( map, 0.6, {scaleX:currentMapZoom, scaleY:currentMapZoom, x:newX, y:newY} );
}
}
复制代码
其中,关于地图缩放时为了保证在地图缩放过程中视野固定,确实花了我不少功夫去研究算法,详细的一些思想在我的博文中也都图文解释过了,虽然如此,我想大多数应该还是无法一时半会能够领悟进去的,若是你领悟了,就差不多可以去解答一些论坛里有关地图中心点缩放算法的提问帖子了,没准还能弄个AQ小生的徽章来戴戴。上面这段代码虽然就100来行,但是我估计很多人会停留在这个页面半小时甚至更长的时间,边看代码边画图能够帮助你更好地思考算法,就算有些公式你看不懂原理,但是你如果知道怎么使用也是不错的,可以直接到处copy我的代码来使用,也能够正常地为你所用。 除了地图缩放算法外,以上代码中还需要注意以下几点: 1,cacheAsBitmap属性的使用:将此属性设置为true后能让一个显示对象缓存为位图,它就不需要flash player在每一帧都进行渲染,可以有效地增加拖拽地图流畅度,在显示元素很多的视图中灵活运用之能够大大提高运行效率,不过若你对一个每一帧都要改变外形的动画对象使用此属性没有任何意义,因为它每一帧都需要flash player重绘之,不可能停止渲染,因此此属性只对静态显示对象有明显效果。更多知识请看 这里 2,切换镜头:双击地图时能够切换镜头,镜头切换原理见下图: <ignore_js_op>
我们双击了白色十字点位置欲把此位置移至舞台中心点红色十字位置处,那么地图图片就会向左上移动至深蓝色框框标志的位置,不难计算出红色与白色十字点间的横坐标位置应该是x - stage.stageWidth/2,y方向位置同理可得。
3.使用TweenLite的尴尬之处: 想让地图缩放有个动态效果就想到使用TweenLite(不知道怎么使用TweenLite?看上一篇教程吧),但是用TweenLite.to方法使指定的参数是属性变化的目标值,但是这个目标值还需要事先把scale放大后才能计算得知,因此我不得不先把scale放大之后计算得到我们需要的地图位置变化目标值之后再把scale恢复为原来的值,然后再次使用TweenLite.to方法来进行放大,此方法有点烂,但是我暂时想不出什么好的方法,若有兄弟有好的建议可以提出来哈。 4.代码中用到的类MyButton.as在上一案例中有提到,以后我将会多次使用它来作为负责交互的按钮。 动态地图 动态地图和静态地图有着很大的不同,因为它所需要显示的不再是一个概览图,而是需要呈现给用户看到地图上每个元素的信息,比如一张城市地图,用户不再满足于看看这个城市的建筑物,地形分布情况,而是期望知道每个建筑物的详细信息。此时将会由动态地图来支持我们实现此需求,先看看总体思路与效果演示吧( 跪求一点 ) 光看思路会感觉十分抽象,因此我们将结合代码来看看。 首先自然是为每一个建筑物创建视图(View)和数据对象(VO),为了增强可拓展性,任何时候都应该记得把视图与数据分离。 TileVO.as:
public class TileVO
{
public var type:int;
public var row:int;
public var col:int;
public function TileVO( type:int=0, row:int=0, col:int=0 )
{
this.type = type;
this.row = row;
this.col = col;
}
}
复制代码
TileView.as:
public class TileView extends Sprite
{
public var Position:Point;//记录地块行列位置
private var _tileVO:TileVO;
private var view:Bitmap = new Bitmap();
public function TileView()
{
addChild( view );
}
public function get tileVO():TileVO
{
return _tileVO;
}
public function set tileVO(value:TileVO):void
{
//若原先没有tileVO或者两次类型不同则换图片外形
var isDifferent:Boolean = _tileVO != null && _tileVO.type != value.type;
if( _tileVO == null || isDifferent ){
_tileVO = value;
switch( _tileVO.type ){
case 0:
view.bitmapData = AssetsManager.getInstance().getResourceByName( "building1" );
break;
case 1:
……
default:
break;
}
}
}
}
复制代码
接下来我们需要思考一下地图上所有元素的存放问题了,在上面给出的博文中讲到,全部地图元素数据一般会存在一个二维数组中(在数据库中就会是一张表格,表格中记录元素行列号),每个元素在地图上所处的行列号就是它在二维数组中的索引位置。一看到这种格子型的数据分布就想到了在 案例15 中提到的Grid类,我们再复习一下此类的结构: Grid.as:
/**
* 格子类
* @author S_eVent
*
*/
public class Grid
{
public var cellsInfo:Array;
private var _rowNum:int = 0;
private var _colNum:int = 0;
private var _maxCells:int;
public function Grid( rowNum:int, colNum:int )
{
cellsInfo = new Array( rowNum );
for( var i:int=0; i<rowNum; i++ ){
cellsInfo[i] = new Array( colNum );
}
this.rowNum = rowNum;
this.colNum = colNum;
}
/**
* 设置某个格子的信息
* @param row 欲设置格子的行号
* @param col 欲设置格子的列号
* @param view 欲设置格子的对象信息
*
*/
public function setCell( row:int, col:int, value:Object):void{
//查询被设置对象是否已在格子中,若在,就把他原来的位置和现在的位置信息互换,若不在,则直接把此格子中填充进信息
var pos:Array = getCell(value);
if( pos.length > 0 ){
var temp:Object = cellsInfo[row][col];
cellsInfo[row][col] = value;
cellsInfo[pos[0]][pos[1]] = temp;
}else{
cellsInfo[row][col] = value;
}
}
/**
* 查询某个物品对象所在格子
* @param view 欲查询对象
* @return 位置数组,格式如[row, col],若查询不到,则返回空数组
*
*/
public function getCell( value:Object ):Array{
for(var i:int=0; i<cellsInfo.length; i++){
for(var j:int=0; j<cellsInfo[i].length; j++){
if( cellsInfo[i][j] == value ){
return [i,j];
}
}
}
return [];
}
/**
* 将某格子数组中某位置处元素删去
*
*/
public function removeItemAt( row:int, col:int ):void{
cellsInfo[row][col] = null;
}
/**
* 检查目标格位置是否已存在元素
* @param row
* @param col
* @return true和false分别表示目标位置已经存在和不存在建筑
*
*/
public function checkIfExist( row:int, col:int ):Boolean{
return cellsInfo[row][col] != null
}
…………
}
复制代码
这里我只列出一些案例中用得到的方法,我们看到Grid类就是一个专门操作和管理二维数组格子的“格子管理员”,一切对数据的操作都会由它来做。 最后看到我们的文档类全部代码: ASMapDynamic.as:
/**
* 动态地图类
* author S_eVent
*/
[SWF(width="800", height="600")]
public class ASMapDynamic extends Sprite
{
private var map:Sprite = new Sprite();
private var oldPoint:Point = new Point();//拖拽前的位置
private var currentMapZoom:Number = 1;//当前地图缩放比例
private var maxZoomLevel:int = 5;//最大可放大次数
private var minZoomLevel:int = -3;//最大可缩小次数
private var currentZoomLevel:int = 1;//当前缩放次数
private var zoomValue:Number = 0.1;//每次放大或缩小的比例
private var centerPoint:Point = new Point();
private var rowNum:int = 50;
private var colNum:int = 50;
private var mapArray:Array = new Array();
private var backgroundImgData:BitmapData;
private var tileLoadRange:int = 5;//每次在中心点左右各要加载的建筑数
private var backgroundImgList:Array = new Array();
private var tileList:Array = new Array();
private var buildingGrid:Grid;
private var dragRect:Rectangle;
private const TILE_SIZE:int = 32;//地块大小
private const BUILDING_SIZE:int = 120;//建筑大小
public function ASMapDynamic()
{
stage.scaleMode = StageScaleMode.NO_SCALE;
initData();
initView();
}
/**
* 生成地图数据
*
*/
private function initData():void{
for( var i:int=0; i<rowNum; i++ ){
for( var j:int=0; j<colNum; j++ ){
//随机生成土地类型,为平地的概率为50%,其他8种建筑类型概率均为1/16
var type:int = int( Math.random() * 16 );
mapArray.push( new TileVO(type, i, j) );
}
}
buildingGrid = new BuildingGrid( rowNum, colNum );//生成建筑物表格
var W:Number = stage.stageWidth - colNum * BUILDING_SIZE;
var H:Number = stage.stageHeight - rowNum * BUILDING_SIZE;
dragRect = new Rectangle(W, H, -W, -H);//限制拖拽范围
}
private function initView():void{
initBackground();
generateTiles();
}
private function initBackground():void{
addChild( map );
//处理素材,拿到草地素材的bitmapData
var bmd:BitmapData = AssetsManager.getInstance().getResourceByName( "Tiles" );
backgroundImgData = AnimationFactory.Cut( bmd, TILE_SIZE, TILE_SIZE, 2, 6 );
centerPoint = tileToPos( 25, 25 );//暂且设中心点位置在地图中央
//为当前中心点附近可视区域铺设草地背景
for( var i:int = centerPoint.x - 30 * TILE_SIZE; i<centerPoint.x + 30 * TILE_SIZE; i += TILE_SIZE ){
for( var j:int = centerPoint.y - 30 * TILE_SIZE; j<centerPoint.y + 30 * TILE_SIZE; j += TILE_SIZE ){
var tempBmp:Bitmap = new Bitmap( backgroundImgData );
tempBmp.x = i;
tempBmp.y = j;
map.addChild( tempBmp );
backgroundImgList.push( tempBmp );
}
}
//设置初始镜头为当前中心点
setCamera( centerPoint.x, centerPoint.y );
map.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
}
/**
* 读取并生成建筑
*
*/
private function generateTiles():void{
for( var i:int = centerPoint.x - tileLoadRange * BUILDING_SIZE; i<centerPoint.x + tileLoadRange * BUILDING_SIZE; i += BUILDING_SIZE ){
for( var j:int = centerPoint.y - tileLoadRange * BUILDING_SIZE; j<centerPoint.y + tileLoadRange * BUILDING_SIZE; j += BUILDING_SIZE ){
var tile:Point = posToTile( i, j );
var vo:TileVO = mapArray[tile.x * colNum + tile.y] as TileVO;
if( vo.type <= 7 && !buildingGrid.checkIfExist(tile.x, tile.y) ){
var view:TileView = new TileView();
view.tileVO = vo;
map.addChild( view );
view.x = i;
view.y = j;
view.Position = tile;
buildingGrid.setCell( tile.x, tile.y, view );
}
}
}
}
复制代码
/**
* 行列数转换为坐标位置
*
*/
private function tileToPos( row:int, col:int ):Point{
return new Point( col * BUILDING_SIZE, row * BUILDING_SIZE );
}
/**
* 你懂的
*/
private function posToTile( x:Number, y:Number ):Point{
var row:int = Math.min( Math.max( int( y / BUILDING_SIZE ), 0 ), rowNum - 1 );
var col:int = Math.min( Math.max( int( x / BUILDING_SIZE ), 0 ), colNum - 1 );
return new Point( row, col );
}
private function setCamera( x:Number, y:Number ):void{
var desX:Number = map.x + stage.stageWidth / 2 - x;
var desY:Number = map.y + stage.stageHeight / 2 - y;
map.x = desX;
map.y = desY;
}
private function onMouseDown(event:MouseEvent):void{
map.startDrag(false, dragRect);
oldPoint.x = map.x;
oldPoint.y = map.y;
map.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
stage.addEventListener(Event.MOUSE_LEAVE, onMouseUp);
}
private function onMouseUp(event:Event):void{
map.stopDrag();
var distX:Number = map.x - oldPoint.x;
var distY:Number = map.y - oldPoint.y;
centerPoint.offset( -distX, -distY );//中心点偏移
//移除可视范围外的地块,增加新进入可视范围的地块
removeTile();
generateTiles();
map.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp);
stage.removeEventListener(Event.MOUSE_LEAVE, onMouseUp);
}
/**
* 移除可视范围外的地块
*
*/
private function removeTile():void{
var index:int = 0;//用以拿到地块数组中每一个位置的索引
//不需要重新创建地块,只需要把原来的地块移动位置即可
for( var i:int = centerPoint.x - 30 * TILE_SIZE; i<centerPoint.x + 30 * TILE_SIZE; i += TILE_SIZE ){
for( var j:int = centerPoint.y - 30 * TILE_SIZE; j<centerPoint.y + 30 * TILE_SIZE; j += TILE_SIZE ){
backgroundImgList[index].x = i;
backgroundImgList[index].y = j;
index++;
}
}
var minTile:Point = posToTile( centerPoint.x - tileLoadRange * BUILDING_SIZE, centerPoint.y - tileLoadRange * BUILDING_SIZE );
var maxTile:Point = posToTile( centerPoint.x + tileLoadRange * BUILDING_SIZE, centerPoint.y + tileLoadRange * BUILDING_SIZE );
//移掉显示范围外的建筑
for( i=0; i<buildingGrid.cellsInfo.length;i++ ){
for( j=0; j<buildingGrid.cellsInfo[i].length; j++ ){
if( buildingGrid.cellsInfo[i][j] != null ){
var view:TileView = buildingGrid.cellsInfo[i][j] as TileView;
if( view.Position.x < minTile.x || view.Position.x > maxTile.x
|| view.Position.y < minTile.y || view.Position.y > maxTile.y){
buildingGrid.removeItemAt(i, j);
map.removeChild( view );
}
}
}
}
}
}
复制代码 代码都集中到文档类里面来做是个极不好的习惯,不过为了简明一些我也顾不得什么了,文件一多估计又有人叫着说头被转晕了。看看这一大坨代码都干了些啥。 首先我们来到了initData方法,这里呢,由于寡人没有数据库,就模仿后台数据库随机生成了一份地图表格数据,并放入了一个叫做mapArray的数组中保存。我们每次移动视角后都会找mapArray来索要数据,若你有配置数据库就不需要做这一步喽,直接问数据库去拿数据吧。之后,我们根据设置的行列数来生成建筑物表格以及设置地图拖拽范围。建筑物表格将会用来保存已创建的建筑物视图信息。 接下来我军又来到了initBackground方法中,先向素材管理员AssetsManager要一张素材的bitmapData,看看AssetsManager源码:
/**
* 素材管理员,单例
* @author S_eVent
*
*/
public class AssetsManager
{
[Embed(source="assets/building1.png")]
public var building1:Class;
[Embed(source="assets/building2.png")]
public var building2:Class;
……
private static var instance:AssetsManager = null;
/**
* 获取素材名对应素材的bitmapData
*
*/
public function getResourceByName( name:String ):BitmapData{
var theClass:Class = this[name] as Class;
return ( new theClass() as Bitmap ).bitmapData;
}
public static function getInstance():AssetsManager{
return instance ||= new AssetsManager();
}
}
复制代码
所有素材都作为一个私有的Class属性嵌在其中,AssetsManager是一个单例,我们可以通过他开放的接口getResourceByName方法来获取我们所需要的图片素材,此方法将返回一个bitmapData实例。注意这种写法“this[name]”是取当前类中名称为name的属性,比如this[“x”]就等于this.x,有的人以为只有通过点操作符才能访问某一个属性,但是通过点操作符并不能动态地访问,比如有一个叫做mc的对象,你想点击这个按钮改变它的x,点击那个按钮改变它的y,你就可以这么干:
private var p:String;
private function onClick1(e:Event):void{
p = "x";
change();
}
private function onClick2(e:Event):void{
p = "y";
change();
}
private function change():void{
mc[p] = 5;
}
复制代码
这样可以动态地访问我们需要的属性。还有一句:
return instance ||= new AssetsManager();
等于:
if( instance == null )instance = new AssetsManager();
return instance ;
复制代码
之后再用代码切图技巧从一张大图上面 <ignore_js_op> 切得我们想要的背景素材——草地。看看AnimationFactory.Cut方法先:
/**
* 动画工厂
* @author S_eVent
*
*/
public class AnimationFactory
{
/**
* 切图
* @param source 切割源
* @param width 切块宽度
* @param height 切块高度
* @param row 欲切割行
* @param col 欲切割列
* @return
*
*/
public static function Cut( source:BitmapData, width:Number, height:Number, row:int, col:int ):BitmapData{
var result:BitmapData = new BitmapData( width, height );
result.copyPixels( source,
new Rectangle( col * width, row * height, width, height ), new Point( 0, 0 ) );
return result;
}
}
复制代码
若对此切图方式原理有所疑惑,请阅读我的 案例10 . 得到背景素材后,我设置了中心点位置为25行25列所对应的x,y坐标,并在中心点位置四周一定区域内铺设草地。为了防止每次拖动地图都要new一个新的背景草地的bitmap实例造成的浪费,我们把第一次创建的这些草地bitmap实例放入数组中便于重用它们。之后,我们把镜头设置为中心点所在位置(setCamera方法的原理还是看1楼吧). 接下来我军又又来到了generateTiles方法中,这是一个很关键的方法,它负责生成当前中心点附近可视区域内的建筑物视图。里面的代码不需要我过多解释,仅让我们睁大我们的狗眼,细细地品味它到底是怎么做到的从地图数据表中获取数据到创建视图这样的一个过程的吧。 至于tileToPos和posToTile方法,反应的仅是一种建筑物布局规则,所以我只能用一句“你懂的”来带过。 接下来我军又又又来到了onMouseDown以及onMouseUp方法中,值得注意的是,在Flash Player中如果你一开始就监听了MOUSE_UP事件,当你鼠标移出FP窗口再移回来时也有可能触发MOUSE_UP事件,即使你之前没有按下过鼠标进行地图拖动,我们期望唯有在按着鼠标拖动过程中松开按键才触发MOUSE_UP事件来生成新的一片建筑群,所以把添加MOUSE_UP的侦听语句放在按下鼠标之后,并在触发MOUSE_UP后移除对其的侦听。还有一点就是在之前的案例中提到过的,为了防止按住鼠标进行地图拖动时把鼠标移出FP窗口后放开鼠标再移回来会造成的由于没有触发MOUSE_UP事件而使地图一直处于被拖动状态的“鼠标跟随”现象,我侦听了stage的MOUSE_LEAVE事件使鼠标移出窗口时能够让地图解除被拖动状态。在onMouseUp方法中我们计算了地图被拖动的距离并以此偏移中心点,之后移除可视范围外的地块,增加新进入可视范围的地块。 接下来我军又又又又来到了removeTile方法中,这个方法负责移除可视范围外的地块,在此方法中我们看到我们对之前创建的草地背景的重用方式以及如何判定地图Grid中之前创建的一个建筑物是否处于可视范围中。 纵观整个流程,可以总结以下几点: 1.地图的拖动不过是在不断改变中心点的位置而已; 2.可视区域是由中心点确定的; 3.为了应用程序使用的流畅,节省资源,可选用每次只显示可视区域的建筑的方式,每一次的拖动完成后都会根据可视区域内的行列号去询访数据源得到对应位置的信息,之后根据信息创建视图; 4.地图数据源中存放的元素均为地图对象,你可以在TileVO中添加任何你感兴趣的信息属性,并在TileView中以ToolTip的方式显示出来,这就提供了一种用户对得知地图每个元素详细信息的需求的方式。 好啦,啰嗦就啰嗦到这里,接下来给出源码供列位仙家细品: <ignore_js_op> src.rar (1.1 MB, 下载次数: 1010) 感谢RenderMonkey爱卿为我处理图片素材。今后的帖子更新速度可能会进一步放慢,还请列位boys见谅~ 楼下回帖的兄弟请勿回复一些我看厌了的内容,比如什么“学习了”,“支持楼主”,“路过”,“顶”(顶你个肺啊顶~)之类的,多谢啦~