Silverlight MMORPG网页游戏开发课程[一期] 第六课:场景之地图与遮挡

引言

关于精灵控件的编写暂告一段落;俗话说美女配香车,完美的精灵少不了与之相匹配的场景。平时我们玩游戏所见的场景平整而华丽;其实游戏中关于场景之地图、遮挡的处理技术手法相当多样,比如切片、分块、延伸、定点虚化等等。本节我将从场景的构建说起,并依次实现2D-RPG游戏中的地图与遮挡效果。

6.1场景地图跟随视角实现 (交叉参考:主位式地图移动模式  牵引式地图移动模式  牵引式地图移动模式  )

        与创建精灵控件一样,我赋予场景同样是一个继承自Canvas的类Scene。游戏中一切可能发生相互作用的对象均以子控件的形式存在于场景中,同时根据RPG游戏的特殊性,我们通常还需要对场景进行分层处理;比如由下至上分别可能包含有背景地图层、坐标系层、参照系层、对象容器层、遮挡物层等等。本节我们首先实现场景中地图背景、精灵容器的基础构建:

 

代码
         ///   <summary>
        
///  获取或设置X、Y坐标
        
///   </summary>
         public  Point Coordinate {
            
get  {  return   new  Point(Canvas.GetLeft( this +  Center.X, Canvas.GetTop( this +  Center.Y); }
            
set  { Canvas.SetLeft( this , value.X  -  Center.X); Canvas.SetTop( this , value.Y  -  Center.Y); }
        }

        
///   <summary>
        
///  获取或设置名称
        
///   </summary>
         public   string  FullName {  get set ; }

        
///   <summary>
        
///  获取或设置地图宽
        
///   </summary>
         public   double  MapWidth {
            
get  {  return  map.Width; }
            
set  { map.Width  =  value; }
        }

        
///   <summary>
        
///  获取或设置地图高
        
///   </summary>
         public   double  MapHeight {
            
get  {  return  map.Height; }
            
set  { map.Height  =  value; }
        }

        
///   <summary>
        
///  获取或设置中心
        
///   </summary>
         public  Point Center {  get set ; }

        Image map 
=   new  Image();
        Canvas container 
=   new  Canvas();  // 对象容器
         public  List < Sprite >  Sprites  =   new  List < Sprite > ();  // 精灵表
         public  Scene() {
            
// 按层次加载容器
             this .Children.Add(map);
            
this .Children.Add(container);
        }

    同时定义好场景对外公开的添加子对象、精灵的方法以适应游戏中时时变化的情况:

 

代码
         ///   <summary>
        
///  添加UIElement对象进容器
        
///   </summary>
        
///   <param name="target"> UIElement对象(不记录) </param>
         public   void  AddUIElement(UIElement target) {
            container.Children.Add(target);
        }

        
///   <summary>
        
///  移除UIElement对象进容器
        
///   </summary>
        
///   <param name="target"> UIElement对象(不记录) </param>
         public   void  RemoveUIElement(UIElement target) {
            container.Children.Remove(target);
        }

        
///   <summary>
        
///  添加精灵
        
///   </summary>
        
///   <param name="sprite"> 精灵对象 </param>
         public   void  AddSprite(Sprite sprite) {
            Sprites.Add(sprite);
            container.Children.Add(sprite);
        }

        
///   <summary>
        
///  移除精灵
        
///   </summary>
        
///   <param name="sprite"> 精灵对象 </param>
         public   void  RemoveSprite(Sprite sprite) {
            Sprites.Remove(sprite);
            RemoveContainerSprite(sprite);
        }

        
///   <summary>
        
///  清空精灵
        
///   </summary>
         public   void  ClearSprites() {
            Sprites.ForEach(X 
=>  RemoveContainerSprite(X));
            Sprites.Clear();
            GC.Collect();
        }

        
///   <summary>
        
///  移除容器中的精灵
        
///   </summary>
        
///   <param name="sprite"></param>
         void  RemoveContainerSprite(Sprite sprite) {
            sprite.Dispose();
            container.Children.Remove(sprite);
            sprite 
=   null ;
        }

    最后还是以同样的方法通过一个Code属性将场景信息的动态下载,地图加载等逻辑进行封装:

 

         int  _Code  =   - 1 ;
        
///   <summary>
        
///  获取或设置代号
        
///   </summary>
         public   int  Code {
            
get  {  return  _Code; }
            
set  {
                
if  (_Code  !=  value) {
                    _Code 
=  value;
                    Downloader configDownloader 
=   new  Downloader() { TargetCode  =  value };
                    configDownloader.Completed 
+=   new  EventHandler < DownloaderEventArgs > (configDownloader_Completed);
                    configDownloader.Download(Global.WebPath(
string .Format( " Scene/{0}/Info.xml " , value)));
                    ClearSprites(); 
// 清空精灵
                }
            }
        }

        
///   <summary>
        
///  场景配置文件下载完毕
        
///   </summary>
         void  configDownloader_Completed( object  sender, DownloaderEventArgs e) {
            Downloader configDownloader 
=  sender  as  Downloader;
            configDownloader.Completed 
-=  configDownloader_Completed;
            
int  code  =  configDownloader.TargetCode;
            
string  key  =   string .Format( " Scene{0} " , code);
            
if  (e.Stream  !=   null ) { Global.ResInfos.Add(key, XElement.Load(e.Stream)); }
            
// 通过LINQ2XML解析配置文件
            XElement xScene  =  Global.ResInfos[key].DescendantsAndSelf( " Scene " ).Single();
            
// 加载地图参数
            FullName  =  xScene.Attribute( " FullName " ).Value;
            MapWidth 
=  ( double )xScene.Attribute( " MapWidth " );
            MapHeight 
=  ( double )xScene.Attribute( " MapHeight " );
            
// 下载缩略图
            Downloader miniMapDownloader  =   new  Downloader() { TargetCode  =  code };
            miniMapDownloader.Completed 
+=   new  EventHandler < DownloaderEventArgs > (miniMapDownloader_Completed);
            miniMapDownloader.Download(Global.WebPath(
string .Format( " Scene/{0}/MiniMap.jpg " , code)));
        }

        
///   <summary>
        
///  场景Mini地图背景下载完毕
        
///   </summary>
         void  miniMapDownloader_Completed( object  sender, DownloaderEventArgs e) {
            Downloader miniMapDownloader 
=  sender  as  Downloader;
            miniMapDownloader.Completed 
-=  miniMapDownloader_Completed;
            
int  code  =  miniMapDownloader.TargetCode;
            
// 用缩略图填充地图背景
            map.Source  =  Global.GetWebImage( string .Format( " Scene/{0}/MiniMap.jpg " , code));
            
// 下载实际地图
            Downloader realMapDownloader  =   new  Downloader() { TargetCode  =  code };
            realMapDownloader.Completed 
+=   new  EventHandler < DownloaderEventArgs > (realMapDownloader_Completed);
            realMapDownloader.Download(Global.WebPath(
string .Format( " Scene/{0}/RealMap.jpg " , code)));
        }

        
///   <summary>
        
///  场景实际地图背景下载完毕
        
///   </summary>
         void  realMapDownloader_Completed( object  sender, DownloaderEventArgs e) {
            Downloader realMapDownloader 
=  sender  as  Downloader;
            realMapDownloader.Completed 
-=  realMapDownloader_Completed;
            
// 呈现实际地图背景
            map.Source  =  Global.GetWebImage( string .Format( " Scene/{0}/RealMap.jpg " , realMapDownloader.TargetCode));
        }

    我的思路是在场景切换时(修改场景的Code),首先下载该场景的xml配置文件并解析其中的相关参数信息赋值到具体属性;接下来下载该代号场景的缩略地图(MiniMap)Fill的形式模糊填充满整个背景(这两步实际上应该在过场画面的后台进行处理,后续章节会对相关知识进行详细讲解);最后再下载实际地图图片(RealMap),完成后替换掉缩略图进行呈现。

在完成场景的构建后,接下来我们就可以轻松实现主角在场景中的地图镜头跟随视角效果了(即传统RPG游戏中主角在除地图边缘外均始终处于屏幕正中间位置)。以主角为中心的镜头跟随依据的当然是主角的坐标属性变化,因此我们首先得为精灵类添加一个坐标改变时触发的事件,当坐标属性改变时触发:

MainPage中为主角注册该事件,并编写主角与场景之间的交互逻辑以实现镜头跟随效果:

代码
         ///   <summary>
        
///  坐标改变时触发
        
///   </summary>
         public   event  CoordinateEventHandler CoordinateChanged;

        
///   <summary>
        
///  获取或设置坐标(关联属性,又称:依赖属性)
        
///   </summary>
         public  Point Coordinate {
            
get  {  return  (Point)GetValue(CoordinateProperty); }
            
set  { SetValue(CoordinateProperty, value); }
        }
        
public   static   readonly  DependencyProperty CoordinateProperty  =  DependencyProperty.Register(
            
" Coordinate " ,
            
typeof (Point),
            
typeof (Sprite),
            
new  PropertyMetadata(ChangeCoordinateProperty)
        );
        
static   void  ChangeCoordinateProperty(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            Sprite sprite 
=  (Sprite)d;
            Point p 
=  (Point)e.NewValue;
            Canvas.SetLeft(sprite, p.X 
-  sprite.Center.X);
            Canvas.SetTop(sprite, p.Y 
-  sprite.Center.Y);
            Canvas.SetZIndex(sprite, (
int )p.Y);
            
if  (sprite.CoordinateChanged  !=   null ) { sprite.CoordinateChanged(sprite, e); }
        }

    然后我们在

LayoutRoot中而转投到了场景(scene)的怀抱,当主角坐标改变时(移动时),场景会整体向相反的方向移动;同时游戏中鼠标左键点击时我们需要取得的坐标点必须相对于场景的才能最终将这个镜头跟随效果完整实现:Demo,主角的移动行为与场景地图配合是如此之默契;没错,Silverlight开发游戏就是这么简单。

代码
        Scene scene  =   new  Scene() {
            Code  =   0 ,
        };

        Sprite hero 
=   new  Sprite() {
            Code 
=   0 ,
            Direction 
=  SpriteDirection.South,
            Coordinate 
=   new  Point( 100 100 ),
        };

        
public  MainPage() {
            InitializeComponent();
            LayoutRoot.Children.Add(scene);
            scene.AddUIElement(hero);
            hero.CoordinateChanged  +=   new  Sprite.CoordinateEventHandler(hero_CoordinateChanged);
            LayoutRoot.MouseLeftButtonDown 
+=   new  MouseButtonEventHandler(LayoutRoot_MouseLeftButtonDown);
        }

        
///   <summary>
        
///  主角坐标改变时触发场景相反移动以实现镜头跟随效果
        
///   </summary>
         void  hero_CoordinateChanged(Sprite sprite, DependencyPropertyChangedEventArgs e) {
            scene.Coordinate 
=   new  Point(
                GetMapPosition(sprite.Coordinate.X, Application.Current.Host.Content.ActualWidth, scene.MapWidth),
                GetMapPosition(sprite.Coordinate.Y, Application.Current.Host.Content.ActualHeight, scene.MapHeight)
            );
        }

        
///   <summary>
        
///  获取地图偏移位置(X或Y)
        
///   </summary>
        
///   <param name="coordinate"> 参照物坐标(X或Y) </param>
        
///   <param name="containerSize"> 容器尺寸(宽或高) </param>
        
///   <param name="mapSize"> 地图尺寸(宽或高) </param>
        
///   <returns> 偏移量(X或Y方向) </returns>
         double  GetMapPosition( double  coordinate,  double  containerSize,  double  mapSize) {
            
if  (coordinate  -  containerSize  /   2   <=   0 ) {
                
return   0 ;
            } 
else   if  (coordinate  >=  mapSize  -  containerSize  /   2 ) {
                
return  containerSize  -  mapSize;
            } 
else  {
                
return  containerSize  /   2   -  coordinate;
            }
        }

此时大家是否注意到主角不再是存放于

         void  LayoutRoot_MouseLeftButtonDown( object  sender, MouseButtonEventArgs e) {
            
Point destination  =  e.GetPosition(scene);
            hero.RunTo(destination);
        }

    最后我们运行下该

6.2场景中遮挡效果实现(交叉参考:地图遮罩层的实现)

RPG的场景中光有地图背景还不行,虽说是2D如能加上一定程度上的遮挡物配上倾斜的镜头跟随视角实现假3D(传说中的2.5D)那将使得游戏效果更加真实而惟妙惟肖。

Silverlight中可以通过Canvas.SetZIndex来设置对象的Z轴层次,于是我们还是首先创建这个遮挡物对象类:

 

代码
     ///   <summary>
    
///  遮挡物控件
    
///   </summary>
     public   sealed   class  Mask : Canvas {

        
///   <summary>
        
///  获取或设置图片源
        
///   </summary>
         public  ImageSource Source {
            
get  {  return  body.Source; }
            
set  { body.Source  =  value; }
        }

        
///   <summary>
        
///  获取或设置X、Y坐标
        
///   </summary>
         public  Point Coordinate {
            
get  {  return   new  Point(Canvas.GetLeft( this +  Center.X, Canvas.GetTop( this +  Center.Y); }
            
set  { Canvas.SetLeft( this , value.X  -  Center.X); Canvas.SetTop( this , value.Y  -  Center.Y); }
        }

        
///   <summary>
        
///  获取或设置Z层次深度
        
///   </summary>
         public   int  Z {
            
get  {  return  Canvas.GetZIndex( this ); }
            
set  { Canvas.SetZIndex( this , value); }
        }

        
///   <summary>
        
///  获取或设置中心
        
///   </summary>
         public  Point Center {  get set ; }

        Image body 
=   new  Image();
        
public  Mask() {
            
this .CacheMode  =   new  BitmapCache();
            
this .Children.Add(body);
        }
    }

    代码依旧简单且熟悉。遮挡物存在于场景中,每个场景根据实际地图的不同遮挡物亦不同。由于是在现成游戏地图的基础上扣取遮挡物,因此我们需要PS的辅助以实现:

以上两幅图描述的是扣取场景中某一处遮挡物时获取的XYZ坐标分别为:155462633,将该遮挡部分图象扣取出来并保存到该场景代号文件夹下的Mask目录后,我们还需在该场景的xml配置文件中补充上该遮挡物的相关信息:

 

 

< Scene FullName = " 龙门镇 "  MapWidth = " 3200 "  MapHeight = " 1320 " >
  
< Masks >
    
< Mask Code = " 0 "  Opacity = " 0.5 "  X = " 155 "  Y = " 462 "  Z = " 630 "   />
    
......
   </ Masks >
</ Scene >

    然后等待场景中实际地图背景图片下载完成后再解析加载:

 

代码
public  List < Mask >  Masks  =   new  List < Mask > ();  // 遮挡物表

        
///   <summary>
        
///  添加遮挡物
        
///   </summary>
        
///   <param name="mask"> 遮挡物对象 </param>
         public   void  AddMask(Mask mask) {
            Masks.Add(mask);
            container.Children.Add(mask);
        }

        
///   <summary>
        
///  移除遮挡物
        
///   </summary>
        
///   <param name="mask"> 遮挡物对象 </param>
         public   void  RemoveMask(Mask mask) {
            Masks.Remove(mask);
            RemoveContainerMask(mask);
        }

        
///   <summary>
        
///  清空遮挡物
        
///   </summary>
         public   void  ClearMasks() {
            Masks.ForEach(X 
=>  RemoveContainerMask(X));
            Masks.Clear();
        }

        
///   <summary>
        
///  移除容器中的遮挡物
        
///   </summary>
        
///   <param name="mask"></param>
         void  RemoveContainerMask(Mask mask) {
            container.Children.Remove(mask);
            mask 
=   null ;
        }

        
///   <summary>
        
///  场景实际地图背景下载完毕
        
///   </summary>
         void  realMapDownloader_Completed( object  sender, DownloaderEventArgs e) {
            Downloader realMapDownloader 
=  sender  as  Downloader;
            realMapDownloader.Completed 
-=  realMapDownloader_Completed;
            
int  code  =  realMapDownloader.TargetCode;
            
// 呈现实际地图背景
            map.Source  =  Global.GetWebImage( string .Format( " Scene/{0}/RealMap.jpg " , code));
            
// 加载遮挡物
             string  key  =   string .Format( " Scene{0} " , code);
            IEnumerable
< XElement >  iMask  =  Global.ResInfos[key].Element( " Masks " ).Elements();
            
for  ( int  i  =   0 ; i  <  iMask.Count(); i ++ ) {
                XElement xMask 
=  iMask.ElementAt(i);
                Mask mask 
=   new  Mask() {
                    Source 
=  Global.GetWebImage( string .Format( " Scene/{0}/Mask/{1}.png " , code, xMask.Attribute( " Code " ).Value)),
                    Opacity 
=  ( double )xMask.Attribute( " Opacity " ),
                    Coordinate 
=   new  Point(( double )xMask.Attribute( " X " ), ( double )xMask.Attribute( " Y " )),
                    Z 
=  ( int )xMask.Attribute( " Z " )
                };
                AddMask(mask);
            }
        }

    当然,作为正规的游戏开发团队这些遮挡物事实上是属于美工范畴与我们程序员无关。最后的运行效果如下:

值得一提的是,在目前很多WebGame中都有采用一种“定点虚化”的技术来实现遮挡效果,原理非常简单:事先为场景定义好可能会被遮挡的坐标集合,当精灵一旦进入这些坐标时不完全透明处理即可。本节中我也模仿了一小部分区域以该方式去实现:当主角坐标处于1800<=X<=20501080<=Y<=1300时透明度为0.5

         ///   <summary>
        
///  主角坐标改变时触发场景相反移动以实现镜头跟随效果
        
///   </summary>
         void  hero_CoordinateChanged(Sprite sprite, DependencyPropertyChangedEventArgs e) {
            scene.Coordinate 
=   new  Point(
                GetMapPosition(sprite.Coordinate.X, Application.Current.Host.Content.ActualWidth, scene.MapWidth),
                GetMapPosition(sprite.Coordinate.Y, Application.Current.Host.Content.ActualHeight, scene.MapHeight)
            );
            textBlock0.Text 
=   string .Format( " 主角当前坐标: X {0} Y {1} " , ( int )hero.Coordinate.X, ( int )hero.Coordinate.Y);
            
#region  遮挡效果定点虚化实现
            
if  (hero.Coordinate.X  >=   1800   &&  hero.Coordinate.X  <=   2050   &&  hero.Coordinate.Y  >=   1080   &&  hero.Coordinate.Y  <=   1300 ) {
                hero.Opacity  =   0.5 ;
            }  else  {
                hero.Opacity  =   1 ;
            }
            
#endregion
        }

    此实现方式可以完全不使用任何遮挡物图片,减少流量带宽且处理手法简单;当然负面后果也是很明显的:极不准确的遮挡定位及不真实的遮挡效果(只要走到相应坐标即会全身透明,与实际不吻合)

本课小结:RPG游戏以场景为核心,游戏中一切对象的相互关系均发生在其内部,故事围绕着它而展开,这也是以场景为核心的游戏引擎基础搭建。游戏内容与形式的创新固然重要,然而没有扎实的根基和完善的低层框架,游戏的后续开发将遍布荆棘与沼泽,这就是游戏中场景的绝对地位。

本课源码点击进入目录下载

参考资料:中游在线[WOWO世界] Silverlight C# 游戏开发:游戏开发技术

教程Demo在线演示地址http://silverfuture.cn

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值