Silverlight MMORPG网页游戏开发课程[一期] 第八课:场景之切换与动画效果

引言

丰富的关卡与场景是充实游戏的魔法圣器,时而穿过云霄,时而坠入大海,就算是陆地同样可以云雾缭绕、山峦叠嶂;作为玩家,游戏玩累了休息时聆听的可以不仅仅是音乐,作为游戏设计者,你有责任将此时疲惫的他们带进梦幻空间:登上紫禁之颠、长城尽头,潜入亚特兰蒂斯深处与美人鱼结伴嬉戏,尝试一次惬意舒心的休憩之旅又未尝不可?虚幻的游戏同样可以给玩家带来真切的感受,华丽莫测的场景变换开启了这扇通往意念领域的大门。

8.1游戏中场景切换实现(交叉参考:地图间的传送与切换  梦幻西游(Demo) 之 “天人合一”  )

传统游戏两个场景之间切换往往通过呈现一幅游戏相关的宣传背景作为过度,并更新地图、角色、模型等目标场景所必须的一切资源读取加载完毕后才算完成。通过Silverlight开发基于WebMMORPG网页游戏则可简化这一过程,动态按需下载技术使得我们在进入新场景前仅需下载该场景的配置文件及缩略地图等少部分资源即可。

按照该思路,我们首先创建一个名为Transition的过场类:

09130927_U0xm.gif 代码
     ///   <summary>
    
///  过场控件
    
///   </summary>
     public   sealed   class  Transition : Canvas {

        
int  _Code  =   - 1 ;
        
///   <summary>
        
///  获取或设置代号
        
///   </summary>
         public   int  Code {
            
get  {  return  _Code; }
            
set  {
                
if  (_Code  !=  value) {
                    _Code 
=  value;
                    
this .Background  =   new  ImageBrush() {
                        ImageSource 
=  Global.GetProjectImage( string .Format( " Transition/{0}.jpg " , 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 ; }

        
///   <summary>
        
///  适应游戏窗口尺寸
        
///   </summary>
         public   void  AdaptToWindowSize() {
            
this .Width  =  Application.Current.Host.Content.ActualWidth;
            
this .Height  =  Application.Current.Host.Content.ActualHeight;
        }

        
public  Transition() {
            
this .CacheMode  =   new  BitmapCache();
        }

    }

并在场景类中定义两个事件ChangeStartChangeEnd分别放在场景代号改变时及Mini地图下载完毕后:

         int  _Code  =   - 1 ;
        
///   <summary>
        
///  获取或设置代号
        
///   </summary>
         public   int  Code {
            
get  {  return  _Code; }
            
set  {
                
if  (_Code  !=  value) {
                    _Code 
=  value;
                     if  (ChangeStart  !=   null ) { ChangeStart( this null ); }
                    teleports.Clear(); 
// 清空传送点集合
                    ClearMasks();  // 清空遮挡物
                    ClearSprites();  // 清空精灵
                    ClearAnimations();  // 清空动画
                    Downloader configDownloader  =   new  Downloader() { TargetCode  =  value };
                    configDownloader.Completed 
+=   new  EventHandler < DownloaderEventArgs > (configDownloader_Completed);
                    configDownloader.Download(Global.WebPath(
string .Format( " Scene/{0}/Info.xml " , value)));
                }
            }
        }

        
///   <summary>
        
///  Mini地图背景下载完毕
        
///   </summary>
         void  miniMapDownloader_Completed( object  sender, DownloaderEventArgs e) {
            Downloader miniMapDownloader 
=  sender  as  Downloader;
            miniMapDownloader.Completed 
-=  miniMapDownloader_Completed;
            
int  code  =  miniMapDownloader.TargetCode;
            
// 用缩略图填充地图背景(如果异步与同步一致)
             if  (miniMapDownloader.Index  ==  index) { map.Source  =  Global.GetWebImage( string .Format( " Scene/{0}/MiniMap.jpg " , code)); }
            
// 下载实际地图
            Downloader realMapDownloader  =   new  Downloader() { TargetCode  =  code, Index  =  miniMapDownloader.Index };
            realMapDownloader.Completed 
+=   new  EventHandler < DownloaderEventArgs > (realMapDownloader_Completed);
            realMapDownloader.Download(Global.WebPath(
string .Format( " Scene/{0}/RealMap.jpg " , code)));
            
if  (ChangeEnd  !=   null ) { ChangeEnd( this null ); }
        }

        
///   <summary>
        
///  实际地图背景下载完毕
        
///   </summary>
         void  realMapDownloader_Completed( object  sender, DownloaderEventArgs e) {
            Downloader realMapDownloader 
=  sender  as  Downloader;
            realMapDownloader.Completed 
-=  realMapDownloader_Completed;
            
int  code  =  realMapDownloader.TargetCode;
            
// 如果异步与同步一致
             if  (realMapDownloader.Index  ==  index) {
                
// 呈现实际地图背景
                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  =   ; 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 " -  Offset.X, ( double )xMask.Attribute( " Y " -  Offset.Y),
                        Z 
=  ( int )xMask.Attribute( " Z " -  Offset.Y
                    };
                    AddMask(mask);
                }
                
// 加载动画
                IEnumerable < XElement >  iAnimation  =  Global.ResInfos[key].Element( " Animations " ).Elements();
                
for  ( int  i  =   ; i  <  iAnimation.Count(); i ++ ) {
                    XElement xAnimation 
=  iAnimation.ElementAt(i);
                    Animation animation 
=   new  Animation() {
                        Code 
=  ( int )xAnimation.Attribute( " Code " ),
                        Opacity 
=  ( double )xAnimation.Attribute( " Opacity " ),
                        Coordinate 
=   new  Point(( double )xAnimation.Attribute( " X " -  Offset.X, ( double )xAnimation.Attribute( " Y " -  Offset.Y),
                        Z 
=  ( int )xAnimation.Attribute( " Z " -  Offset.Y,
                        Tip 
=  xAnimation.Attribute( " Tip " ).Value,
                    };
                    AddAnimation(animation);
                }
            }
        }

大家需要特别注意场景切换时由于动态下载Mini地图和Real地图,因此逻辑上需要和精灵一样加入异步与同步的协调。

接着在MainPage中为游戏场景注册这两个事件,分别编写以下逻辑:

         ///   <summary>
        
///  游戏窗口尺寸改变
        
///   </summary>
         void  Content_Resized( object  sender, EventArgs e) {
            hero_CoordinateChanged(hero, 
new  DependencyPropertyChangedEventArgs());
            
if  (transition.Visibility  ==  Visibility.Visible) {  transition.AdaptToWindowSize(); }
        }

        
///   <summary>
        
///  场景切换开始
        
///   </summary>
         void  scene_ChangeStart( object  sender, EventArgs e) {
            
hero.CoordinateChanged  -=  hero_CoordinateChanged;
            transition.Code 
=   ;
            
transition.AdaptToWindowSize();
            transition.Visibility 
=  Visibility.Visible;
            LayoutRoot.Children.Add(transition);
        }

        
///   <summary>
        
///  场景切换结束
        
///   </summary>
         void  scene_ChangeEnd( object  sender, EventArgs e) {
            
hero.CoordinateChanged  +=  hero_CoordinateChanged;
            
hero.TeleportTo(teleport.ToCoordinate, (SpriteDirection)teleport.ToDirection);
            transition.Visibility 
=  Visibility.Collapsed;
            LayoutRoot.Children.Remove(transition);
        }

    本节Demo中我仅以最简单的形式来实现场景切换过场效果,即开始->结束,过程中也只是通过一张可以自适应(填充)浏览器尺寸的背景图片作为呈现。实际游戏开发中大家完全可以具体到场景代号改变时的每一个环节;比如配置文件下载完成,Mini地图下载完成,基本素材下载完成,NPC下载完成等位置放置事件并在MainPage中触发,为Transition过场类添加相应控件以显示进度及描述文字。

接下来是如何实现RPG游戏中由主角所触发的场景切换?

当然踩地雷的形式来得最为直接而简单。通过在场景配置文件中添加上相应的传送点信息描述实现,里面记录下会触发传送的坐标,并指明该传送的目的地等信息:

09130927_U0xm.gif 代码
< Scene FullName = " 龙门镇 "  MapWidth = " 3200 "  MapHeight = " 1320 "  OffsetX = " 1600 "  OffsetY = " -1600 "  TerrainGridSize = " 30 "  TerrainGradient = " 60 "  TerrainMatrixDimension = " 128 "  Terrain = " 36_97_0,36_98_0,37_95_0,37_96_0,37_97_0,37_98_0,37_99_0,38_94_0,38_95_0,38_99_0,38_100_0,39_94_0,39_100_0,39_101_0,40_92_0,40_93_0,40_94_0,40_101_0,40_102_0,41_92_0,41_102_0,...... " >
  
< Teleports >
    
< Teleport Code = " 10 "  ToScene = " 1 "  ToX = " 15 "  ToY = " 30 "  ToDirection = " 3 "  Terrain = " 84_39,85_39,86_39,86_38,85_38,84_38 " />
  
</ Teleports >
  ......
</ Scene >

    此时场景类中也需要添加对它内部所包含传送点的解析:

             // 解析传送点
            IEnumerable < XElement >  iTeleport  =  xScene.Element( " Teleports " ).Elements();
            
for  ( int  i  =   ; i  <  iTeleport.Count(); i ++ ) {
                XElement xTeleport 
=  iTeleport.ElementAt(i);
                Teleport teleport 
=   new  Teleport() {
                    Code 
=  ( int )xTeleport.Attribute( " Code " ),
                    ToScene 
=  ( int )xTeleport.Attribute( " ToScene " ),
                    ToCoordinate 
=   new  Point(( double )xTeleport.Attribute( " ToX " ), ( double )xTeleport.Attribute( " ToY " )),
                    ToDirection 
=  (SpriteDirection)( int )xTeleport.Attribute( " ToDirection " ),
                };
                teleports.Add(teleport);
                
string [] teleportTerrain  =  xTeleport.Attribute( " Terrain " ).Value.Split( ' , ' );
                
for  ( int  j  =   ; j  <  teleportTerrain.Count(); j ++ ) {
                    
if  (teleportTerrain[j]  !=   "" ) {
                        
string [] position  =  teleportTerrain[j].Split( ' _ ' );
                        TerrainMatrix[Convert.ToByte(position[
]), Convert.ToByte(position[ 1 ])]  =  ( byte )teleport.Code;
                    }
                }
            }

    最后是在主角坐标改变事件中判断是否踩到了场景的传送点坐标进而触发传送:

         ///   <summary>
        
///  主角坐标改变时触发场景相反移动以实现镜头跟随效果
        
///   </summary>
         void  hero_CoordinateChanged(Sprite sprite, DependencyPropertyChangedEventArgs e) {
            
// 进行场景相对偏移
            scene.RelativeOffsetTo(sprite.Coordinate);
            ......
            
// 判断是否采到传送点
            Teleport tempTeleport  =  scene.InTeleport(sprite.Coordinate);
            
if  (tempTeleport  !=   null ) {
                teleport 
=  tempTeleport;
                scene.Code 
=  teleport.ToScene;
            }
        }

    其中的InTeleport方法如下:

         ///   <summary>
        
///  是否在传送点内
        
///   </summary>
        
///   <param name="p"> 目标点(游戏坐标系) </param>
        
///   <returns> 所处传送点 </returns>
         public  Teleport InTeleport(Point p) {
            
if  (TerrainMatrix  ==   null ) {  return   null ; }
            
int  code  =  TerrainMatrix[( byte )p.X, ( byte )p.Y];
          
   if  (code  >=   10 ) {
                
return  teleports.Single(X  =>  X.Code  ==  code);
            } 
else  {
                
return   null ;
            }
        }

    这里我硬性的规定在场景的地形数组TerrainMatrix中只要是>=10的都被用做传送点,该数字对应传送点的Code属性。补充说明一下,这样的方式对于A*寻路会有一定影响,我们可添加一个新的名为teleportMatrix的传送矩阵来保存这些传送点,独立于地形数组,也不必强迫Code值从10开始,当然这就意味着场景类中需要多维护一个与TerrainMatrix一样维度的矩阵,综合利弊,在下一节中我将改用方式。

到此我们就完成了场景的传送功能,以号场景为例,它包含这样的传送点信息:

  <Teleport Code="10" ToScene="1" ToX="15" ToY="30" ToDirection="3" Terrain="84_39,85_39,86_39,86_38,85_38,84_38"/>

    那么它将意味着只要主角走到(84,39)(85,39)(86,39)(86,38)(85,38)(84,38)6个坐标(它们对应场景中TerrainMatrix的值均为Code,比如TerrainMatrix[84,39]=10)中任意一个时,都会被传送到1号场景的(15,30)坐标,朝向东南。

离完美的传送效果似乎还有一定距离,遗漏了些什么?

是的,我们仅仅是从逻辑上实现了相应功能,我们还缺少一个传送装置,缺少传送时那华丽的光环萦绕一身的效果。

同样都是动画的表现形式,那么首先我们还得从创建动画控件出发:

09130927_U0xm.gif 代码
     ///   <summary>
    
///  动画控件
    
///   </summary>
     public   class  Animation : Canvas {

        
#region  属性

        
#region  动态

        
#region  封装代号逻辑

        
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( " Animation/{0}/Info.xml " , value)));
                }
            }
        }

        
///   <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( " Animation{0} " , code);
            
if  (e.Stream  !=   null ) { Global.ResInfos.Add(key, XElement.Load(e.Stream)); }
            
// 通过LINQ2XML解析配置文件
            XElement xAnimation  =  Global.ResInfos[key].DescendantsAndSelf( " Animation " ).Single();
            
// 加载动画参数
            FullName  =  xAnimation.Attribute( " FullName " ).Value;
            Center 
=   new  Point(( double )xAnimation.Attribute( " CenterX " ), ( double )xAnimation.Attribute( " CenterY " ));
            frameNum 
=  ( int )xAnimation.Attribute( " FrameNum " );
            dispatcherTimer.Interval 
=  TimeSpan.FromMilliseconds(( int )xAnimation.Attribute( " Interval " ));
            format 
=  Global.GetFileFormat((FileFormat)(( int )xAnimation.Attribute( " Format " )));
            Kind 
=  (AnimationKind)( int )xAnimation.Attribute( " Kind " );
            
// 解析各帧偏移
            IEnumerable < XElement >  iFrame  =  Global.ResInfos[key].Elements();
            frameOffset 
=   new  Point2D[iFrame.Count()];
            
foreach  (XElement element  in  iFrame) {
                frameOffset[(
int )element.Attribute( " ID " )]  =   new  Point2D() {
                    X 
=  ( int )element.Attribute( " OffsetX " ),
                    Y 
=  ( int )element.Attribute( " OffsetY " ),
                };
            }
            Coordinate 
=   new  Point(Coordinate.X  +   0.000001 , Coordinate.Y);
            dispatcherTimer.Start();
        }

        
#endregion

        
///   <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 (Animation),
            
new  PropertyMetadata(ChangeCoordinateProperty)
        );
        
static   void  ChangeCoordinateProperty(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            Animation animation 
=  d  as  Animation;
            Point p 
=  (Point)e.NewValue;
            Canvas.SetLeft(animation, p.X 
-  animation.Center.X);
            Canvas.SetTop(animation, p.Y 
-  animation.Center.Y);
        }

        
///   <summary>
        
///  设置提示内容
        
///   </summary>
         public   string  Tip {
            
set  {
                
if  (value  !=   "" ) {
                    ToolTipService.SetToolTip( this , value);
                }
            }
        }

        
#endregion

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

        
///   <summary>
        
///  获取或设置类型
        
///   </summary>
         public  AnimationKind Kind {  get set ; }

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

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

        
#endregion

        
#region  事件

        
///   <summary>
        
///  仅当Kind == AnimationKinds.OnceToDispose时触发
        
///   </summary>
         public   event  EventHandler Disposed;

        
#endregion

        
#region  构造

        
int  currentFrame, frameNum;
        Point2D[] frameOffset;
        
string  format  =   string .Empty;
        Image body 
=   new  Image();
        DispatcherTimer dispatcherTimer 
=   new  DispatcherTimer();
        
public  Animation() {
            
// ObjectTracker.Track(this);
             this .CacheMode  =   new  BitmapCache();
            
this .Children.Add(body);
            dispatcherTimer.Tick 
+=   new  EventHandler(dispatcherTimer_Tick);
        }

        
#endregion

        
#region  方法

        
void  dispatcherTimer_Tick( object  sender, EventArgs e) {
            
if  (currentFrame  ==  frameNum) {
                
switch  (Kind) {
                    
case  AnimationKind.Once:
                        currentFrame 
=   ;
                        dispatcherTimer.Stop();
                        
break ;
                    
case  AnimationKind.OnceToDispose:
                        Dispose();
                        
if  (Disposed  !=   null ) { Disposed( this new  EventArgs()); }
                        
return ;
                    
case  AnimationKind.Loop:
                        currentFrame 
=   ;
                        
break ;
                }
            }
            body.Source 
=  Global.GetWebImage( string .Format( @" Animation/{0}/{1}{2} " , Code, currentFrame, format));
            
Canvas.SetLeft(body, frameOffset[currentFrame].X);
            
Canvas.SetTop(body, frameOffset[currentFrame].Y);
            currentFrame
++ ;
        }

        
///   <summary>
        
///  销毁
        
///   </summary>
         public   void  Dispose() {
            dispatcherTimer.Stop();
            dispatcherTimer.Tick 
-=  dispatcherTimer_Tick;
        }

        
#endregion
    }

虽然动画控件的原理与精灵切图类似,在此之上新引入了基于具体帧的偏移(从而不再需要每张图都同样尺寸节省资源空间),比如以0号动画为例,它的配置如下:

09130927_U0xm.gif 代码
<? xml version = " 1.0 "  encoding = " utf-8 "   ?>
< Animation FullName = " 传送点 "  CenterX = " 66 "  CenterY = " 38 "  FrameNum = " 7 "  Interval = " 160 "  Format = " 1 "  Kind = " 2 " >
   < Frame ID = " "  OffsetX = " "  OffsetY = " "   />
  
< Frame ID = " 1 "  OffsetX = " "  OffsetY = " "   />
  
< Frame ID = " 2 "  OffsetX = " "  OffsetY = " "   />
  
< Frame ID = " 3 "  OffsetX = " "  OffsetY = " "   />
  
< Frame ID = " 4 "  OffsetX = " "  OffsetY = " "   />
  
< Frame ID = " 5 "  OffsetX = " "  OffsetY = " "   />
  
< Frame ID = " 6 "  OffsetX = " "  OffsetY = " "   />
</ Animation >

另外赋予动画控件的三种常用模式以满足可能的需求:

     ///   <summary>
    
///  动画类型
    
///   </summary>
     public   enum  AnimationKind {
        
///   <summary>
        
///  仅播放一次后回到第一帧静止
        
///   </summary>
        Once  =   ,
        
///   <summary>
        
///  播放一次结束后自动移除
        
///   </summary>
        OnceToDispose  =   1 ,
        
///   <summary>
        
///  一直循环播放
        
///   </summary>
        Loop  =   2 ,
    }

    动画每播放到结束帧时通过判断是哪种模式进而触发相应逻辑:

         void  dispatcherTimer_Tick( object  sender, EventArgs e) {
            
if  (currentFrame  ==  frameNum) {
                
switch  (Kind) {
                    
case  AnimationKind.Once:
                        currentFrame 
=   ;
                        dispatcherTimer.Stop();
                        
break ;
                    
case  AnimationKind.OnceToDispose:
                        Dispose();
                        
if  (Disposed  !=   null ) { Disposed( this new  EventArgs()); }
                        
return ;
                    
case  AnimationKind.Loop:
                        currentFrame 
=   ;
                        
break ;
                }
            }
            body.Source 
=  Global.GetWebImage( string .Format( @" Animation/{0}/{1}{2} " , Code, currentFrame, format));
            Canvas.SetLeft(body, frameOffset[currentFrame].X);
            Canvas.SetTop(body, frameOffset[currentFrame].Y);
            currentFrame
++ ;
        }

        
///   <summary>
        
///  销毁
        
///   </summary>
         public   void  Dispose() {
            dispatcherTimer.Stop();
            dispatcherTimer.Tick 
-=  dispatcherTimer_Tick;
        }

    另外大家是否有注意到如果为动画控件赋了Tip值,那么动画将会附加ToolTip提示效果,配合上前面的3种模式,该动画控件能适用的范围更加广泛,且能以此为基类继续向下衍生出比如魔法、装饰等控件。

本课小结:本节我向大家讲解了如何实现游戏中场景切换(传送)及动画效果。这也是对游戏框架整体合理性的一次综合考验,在合理封装的游戏设计规范下,仅仅需要改动丁点的代码即可完成复杂的游戏功能拓展,这也是C#开发Silverlight-MMORPG网页游戏给我们所带来的面向对象高效率开发模式所赋予的益处。

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

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

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


原文链接: http://www.cnblogs.com/alamiye010/archive/2010/09/30/1839596.html

转载于:https://my.oschina.net/chen106106/blog/43660

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值