04 设计支持手势操作的 XNA 游戏

摘要

上一回我们为大家介绍了更多的 XNA Framework 支持游戏开发的类别,包括支持输入控制,音效播放,以及背景音乐播放控制的类别等等,让读者能够为所制作的游戏程序加入更丰富的游戏效果。这一回我们将要为大家介绍进阶的输入控制技巧,让使用者可以利用 Windows Phone 7 智能型手机支持多点触控的触控屏幕控制游戏程序的执行。

认识手势操作

在上一回的介绍中,我们学会如何利用 TouchPanel 类别的 GetState 方法查询智能型手机的触摸屏的状态,并依据使用者触碰在触控屏幕的位置移动游戏程序显示的图形的位置。

呼叫 TouchPanel 类别的 GetState 方法查询触控屏幕的状态并判断用户触碰触控屏幕的位置只是最简单的触控屏幕控制技巧,除了支持取得触控屏幕的状态以外,触控屏幕还支持用户进行多种不同的控制,包括触碰、触碰不放、水平拖曳、垂直拖曳、自由拖曳、以及轻拂等操作。表1 所示即为触控屏幕的各种状态的说明:

表1:触控屏幕的各种状态的说明
操作动作 说明
Tap 触碰。触碰触控屏幕后放开,没有移动的动作。
DoubleTap 连续触碰。连续触碰同一个位置两次。
Hold 点住不放。触碰后不放达一段时间。
VerticalDrag 垂直拖曳。触碰屏幕后上下移动。
HorizontalDrag 水平拖曳。触碰屏幕后左右移动。
FreeDrag 自由拖曳。触碰屏幕后往任意方向移动
DragComplete 拖曳结束。
Flick 轻拂。触碰屏幕后往任意方向拂动后离开屏幕。
Pinch 同时便用两个手指头触碰触控屏幕后移动。
PinchComplete Pinch 操作结束。

 

以 XNA 为基础的游戏程序可以利用表1所列的各种触控屏幕状态判断用户执行的触控操作种类,以反应使用者的触控操作。

[注意]

以 XNA 为基础的游戏程序必须启用触控功能才能够让游戏的使用者进行触控操作,如果已启用 Pinch 操作功能,则当用户利用两个手指头同时触碰触控屏幕并进行移动时,就会产生 Pinch 操作,而不是两个不同的拖曳操作,如果未启用 Pinch 操作功能,则所产生的就不是 Pinch 操作,而是依据两个触碰位置的平均为准的单一拖曳操作。

启用手势操作支持

以 XNA 为基础的游戏程序必须设定 TouchPanel 类别的 EnabledGestures 属性,才能够启用手势操作功能,以支持用户以手势操作游戏程序。

程序设计师可以在 Game1 类别的 Initialize 方法执行设定 TouchPanel 类别的 EnabledGestures 属性的动作,以启用手势操作支持,做法如下:

TouchPanel.EnabledGestures =
                GestureType.Hold |
                GestureType.Tap | 
                GestureType.DoubleTap |
                GestureType.FreeDrag |
                GestureType.Flick |
                GestureType.Pinch;

[说明]

请注意在上述的程序中,GestureType.FreeDrag 设定表示要支持使用者以自由拖曳的方式操作游戏程序,设定了 GestureType.FreeDrag 就已经涵盖 GestureType.VerticalDrag 设定和 GestureType.HorizontalDrag 设定。而 DragComplete 状态和 PinchComplete 状态代表触控动作结束的状态,不需要启用。

[注意]

以 XNA 为基础的游戏程序可以视需要启用需要使用的触控功能,例如只支持用户利用触控的方式选取菜单的游戏程序,就可以仅启用 Tap 和 VerticalDrag 两种触控功能,让用户以垂直拖曳的方式卷动游戏程序提供的菜单,再触碰欲选择的菜单,其他不需要用到的触控控制功能就不需要启用,避免启用多种触控操作功能,造成判断触控操作动作的逻辑复杂,进而影响到触控的精确度和游戏程序执行的效能。

处理使用者的手势操作

启用了手势操作功能之后,以 XNA 为基础的应用程序可以在 Game1 类别的 Update 方法中呼叫 TouchPanel 类别的 ReadGesture 方法取得用户的手势操作信息。请注意读取使用者的手势操作的做法和呼叫 TouchPanel 类别的 GetState 方法读取触控面板的状态的做法不同,因为用户对游戏程序的触控操作会产生多个手势信息,来不及被游戏处理的手势信息会被存放到队列中等待处理,让游戏程序利用循环取出并加以处理。

以下的 Update 方法便会利用 while 循环,搭配 TouchPanel 类别的 IsGestureAvailable 属性判断是否还有用户触控操作产生的手势信息尚未被处理,如果尚有使用者触控操作产生的手势信息尚未被处理,则呼叫 TouchPanel 类别的 ReadGesture 方法读取手势信息,并加以处理:

protected override void Update(GameTime gameTime)
{
    …
    while (TouchPanel.IsGestureAvailable)		//判斷是否尚有手勢資訊尚未被處理
    {
        GestureSample gesture = TouchPanel.ReadGesture();	//讀取尚未處理的手勢資訊
        switch (gesture.GestureType)						//判斷手勢操作的種類
        {
            case GestureType.Tap:						//如果手勢操作的種類是Tap
			 //處理 Tap 操作
                break;
		  …
        }
    }
}

因为 GestureType.FreeDrag 自由拖曳操作已经包括 GestureType.VerticalDrag 垂直拖曳操作和 GestureType.HorizontalDrag 水平拖曳操作,所以游戏程序在判断用户的触控操作的动作时,不需要既判断动作是否为 GestureType.FreeDrag,又判断动作是否为 GestureType.VerticalDrag 或 GestureType.HorizontalDrag,两者择一处理即可。

[特别注意]

使用 TouchPanel 类别进行触控控制的游戏程序可以呼叫 TouchPanel 类别的 GetState 方法取得使用者对触控面板的触控状态,或是呼叫 TouchPanel 类别的 ReadGesture 方法取得使用者的手势操作状态,不要两者混用,否则将会无法得到正确的结果。例如先利用 TouchPanel 类别的 ReadGesture 方法取得使用者的手势操作状态,再利用 TouchPanel 类别的 GetState 方法取得用户触碰屏幕的位置,当做手势操作触碰屏幕的位置来使用就是错误的做法。

请注意呼叫 TouchPanel 类别的 ReadGesture 方法读取手势信息时,读取到的手势信息会以 GestureSample 结构的型式传回给呼叫者。表2 所示为 GestureSample 结构常用的属性:

表2:GestureSample 结构常用的属性
属性名称 说明
Delta 存放与第一个碰触点的偏差量。
Delta2 存放与第二个碰触点的偏差量。
GestureType 存放触控操作的种类。
Position 存放第一个碰触点的位置。
Position2 存放第二个碰触点的位置。
Timestamp 存放触控操作发生的时间。

 

因为呼叫 TouchPanel 类别的 ReadGesture 方法传回的 GestureSample 结构只能存放第一个和第二触碰点的位置:Position 和 Postition2,以及存放两个触碰点的偏差量,所以只能支持到最多两个手指头的操作,事实上 Windows Phone 7 配备的触控屏幕最多可以支持到多达四个触碰点的触控操作,程序可以经由呼叫 TouchPanel 类别的 GetCapabilities 方法查询触控屏幕的基本功能,取得 TouchPanelCapabilities 结构型态的传回值之后,就可以利用 TouchPanelCapabilities 结构的 MaximumTouchCount 成员得知触控屏幕最大支持的触碰点数,做法如下:

TouchPanelCapabilities tcs = TouchPanel.GetCapabilities();	//查詢觸控螢幕的基本功能

有关 TouchPanelCapabilities 结构常用的属性请参考表3 的说明:

表3:TouchPanelCapabilities 结构常用的属性
属性名称 说明
IsConnected 查询触控屏幕的可用状态。
MaximumTouchCount 取得支持使用者同时触碰触控屏幕的触碰点数量。

 

如果需要取得用户同时触碰屏幕的所有坐标点,则可以呼叫 TouchPanel 类别的 GetState 方法,取得所有触碰点的集合,再利用循环取出集合中的所有触碰点并加以处理,做法如下:

TouchCollection tc = TouchPanel.GetState();					//取得觸控螢幕的狀態
foreach (TouchLocation tl in tc)							//取得所有的觸碰點
{
    //利用 tl.Position 取得觸碰點座標
}

[说明]

请注意 GestureSample 结构的属性的型态为 TimeSpan 结构而不是 GameTime 类别,用来代表手势操作与手势操作之间的时间间隔。

各种手势操作需要用到的 GestureSample 结构的属性可以参考表4 的详细说明:

表4:各种手势操作需要用到的 GestureSample 结构的属性
触控操作 说明
Tap Position 属性
DoubleTap Position 属性
Hold Position 属性
VerticalDrag Position 属性和 Delta 属性
HorizontalDrag Position 属性和 Delta 属性
FreeDrag Position 属性和 Delta 属性
DragComplete
Flick Delta 属性
Pinch Position、Position2、Delta、和 Delta 2 四个属性
PinchComplete

 

处理手势操作的要诀

在处理使用者的触控操作方面,Flick 触控操作可以利用 GestureSample 结构的 Delta 属性的内容值当做用户轻拂的快慢速度,以控制卷动游戏内容的速度。VerticalDrag 和 HorizontalDrag 触控操作可以利用 GestureSample 结构的 Delta 属性的内容值判断垂直和水平移动的距离,而且 VerticalDrag 触控操作的 Delta 属性的 X 成员的内容值必为 0,而 HorizontalDrag 触控操作的 Delta 属性的 Y 成员的内容值必为 0,不需要另外透过程序代码进行设定。Pinch 触控操作是两个手指头并用的触控操作,常常用来执行旋转对象、放大缩小对象、或是旋转相机镜头的动作。游戏程序可以利用 GestureSample 结构的 Position 属性和 Delta 属性,取得第一个手指头的触碰点和偏差量,利用 Position2 属性和 Delta2 属性取得第二个手指头的触碰点和偏差量,再据以执行变更游戏程序显示的内容的动作。

设计支持手势操作的 XNA 游戏

了解 XNA Framework 支持触控操作的基本功能之后,接下来我们就要设计一个能够允许用户利用触控屏幕操作的简单游戏。

首先请启动 Microsoft Visual Studio 2010 Express,建立型态为 [XNA Game Studio(4.0)] 型态的项目,然后于 Content Pipeline 项目中加入让使用者进行触控操作的图形的圆形档案,以及当圆形碰撞到游戏程序的窗口时欲发出的声响的声音文件。例如本范例准备了两个圆形的图案,名称为 Ball.png 的图形档案是一个圆形,名称为 HoldBall.png 的图形档案是一个中心有一个红点的圆形,用来表示被点选的图形,而 Hitwall.wav 则是当圆形的图案碰撞到游戏程序的窗口时欲发出的声音的声音文件。

将游戏程序需要使用的资源加入到 Content Pipeline 项目之后,请开启游戏项目中的 Game1.cs 档案,于类别中加入以下的变量和属性宣告:

Texture2D Ball;											//管理圓形圖案的變數
Texture2D HoldBall;					              //管理中心有紅點的圓形圖案的變數
Vector2 BallPosition=Vector2.Zero;						//記錄圓形圖案位置的變數
bool isHold = false;				         //記錄使用者是否執行Hold式的觸控操作的變數

SoundEffect HitWall;						//管理圖案碰撞遊戲程式視窗的音效的變數
SoundEffectInstance HitWallEffect;						//管理欲播放的音效的變數

Vector2 Velocity = Vector2.Zero;						//記錄Flick操作速度的變數
public const float Friction = 0.9f;							//記錄摩擦力的變數
public const float BounceMagnitude = .5f;					//記錄反彈速度的變數

public const float MinScale = .5f;						//記載最小縮小比例的變數
public const float MaxScale = 2f;						//記載最大放大比例的變數
private float scale = 1f;									//記載目前比例的變數
public float Scale										//記載目前比例的屬性
{
    get { return scale; }
    set 
    { 
   scale=MathHelper.Clamp(value, MinScale, MaxScale);//控制縮放的比例的最大/最小值
    }
}

准备好游戏程序需要的变量之后,请修改 Game1 类别的建构函式,在建立 GraphicsDeviceManager 类别的对象之后设定 GraphicsDeviceManager 类别的对象的 PreferredBackBufferHeight 属性和 graphics.PreferredBackBufferWidth 属性,将游戏程序的窗口设定成宽 480 x 高 800 的大小。编辑妥的 Game1 类别建构函式如下:
public Game1()
{
    graphics = new GraphicsDeviceManager(this);//建立GraphicsDeviceManager類別的物件
    Content.RootDirectory = "Content";					//設定載入遊戲資源的根路徑
    graphics.PreferredBackBufferHeight = 800;				//設定遊戲程式視窗的高度
    graphics.PreferredBackBufferWidth = 480;				//設定遊戲程式視窗的寬度
    // Frame rate is 30 fps by default for Windows Phone.
    TargetElapsedTime = TimeSpan.FromTicks(333333);	//設定 Update 方法被呼叫的頻率
}

设定妥游戏程序窗口的大小之后请编辑 Game1 类别的 Initialize 方法,负责启用手势操作的功能,编辑妥的 Initialize 方法如下:

protected override void Initialize()
{
    // TODO: Add your initialization logic here
    TouchPanel.EnabledGestures =
    		GestureType.Hold |
    		GestureType.Tap |
          GestureType.DoubleTap |
          GestureType.FreeDrag |
          GestureType.HorizontalDrag |
          GestureType.Flick |
          GestureType.Pinch;								//啟用手勢操作功能
    base.Initialize();
}

启用手势操作功能之后请于 Game1 类别的 LoadContent 方法加入加载游戏资源的程序代码,编辑好的 LoadContent 方法如下:

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    // TODO: use this.Content to load your game content here
    Ball = Content.Load<Texture2D>("Ball");						//載入圓形圖案
    HoldBall = Content.Load<Texture2D>("HoldBall");		//載入中心有紅點的圓形圖案
    BallPosition = new Vector2((Window.ClientBounds.Width - Ball.Width) / 2, 
  (Window.ClientBounds.Height-Ball.Height)/2);//設定圓形圖案要顯示在視窗的正中央

    // TODO: use this.Content to load your game content here
    HitWall = 
   Content.Load<SoundEffect>("HitWall");//載入圓形圖案撞擊視窗邊界要發出的音效檔案
    HitWallEffect = 
HitWall.CreateInstance();					//利用 SoundEffect 類別的物
    //件建立 SoundEffectInstance 類別的物件
    HitWallEffect.Apply3D(new AudioListener(), 
new AudioEmitter());		//呼叫SoundEffectInstance類別的 Apply3D 方法套用 3D 音效
}

您可以视需要修改上述的程序代码加载的资源名称,以加载游戏程序执行时需要的资源。

加载妥游戏程序需要使用的资源之后,请修改名称为 Update 的方法,以反应使用者的手势操作动作。加入手势控制功能的 Update 方法如下:

protected override void Update(GameTime gameTime)
{
    // Allows the game to exit
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        this.Exit();
    // TODO: Add your update logic here
    while (TouchPanel.IsGestureAvailable)			//判斷使用者是否執行了手勢操作
    {
        GestureSample gesture = TouchPanel.ReadGesture();	    //讀取使用者的手勢操作
   switch (gesture.GestureType)					//判斷使用者的手勢操作型態
        {
            case GestureType.Tap:									//Tap 操作
            case GestureType.DoubleTap:							//DoubleTap 操作
                BallPosition.X=gesture.Position.X-Ball.Width/2;    //設定圖案顯示在使用
                BallPosition.Y=gesture.Position.Y-Ball.Height/2;   //者觸碰的座標點
                break;
            case GestureType.Hold:									//Hold 操作
                Rectangle BallRect = new Rectangle(0, 0, (int)(Ball.Width * Scale), 
(int)(Ball.Height * Scale));				//計算圖形的矩形大小
                if (BallRect.Contains((int)(gesture.Position.X - BallPosition.X), 
(int)(gesture.Position.Y - BallPosition.Y)))//判斷觸碰點是否在矩形中
                {
                    isHold = true;				    //將 IsHold 變數的內容值設定為 true
                }
                break;
            case GestureType.FreeDrag:							//FreeDrag 操作
                BallPosition += gesture.Delta;		//循著使用者拖曳的軌跡移動圖形
                break;
            case GestureType.Flick:									//Flick 操作
                Velocity = gesture.Delta;                        //設定移動圖形的速度
                break;
            case GestureType.Pinch:									//Pinch 操作
                Vector2 a = gesture.Position;					   //取得第一個觸碰點
                Vector2 aOld = 
gesture.Position - gesture.Delta;//取得第一個觸碰點的起始位置
                Vector2 b = gesture.Position2;				   //取得第二個觸碰點
                Vector2 bOld = 
gesture.Position2 - gesture.Delta2;//取得第二個觸碰點的起始位置
float d = Vector2.Distance(a, b);			//計算兩個觸碰點之間的距離
                float dOld = Vector2.Distance(aOld, bOld);//計算兩個原始座標之間的距離
                float scaleChange = (d - dOld) * .01f;			  //計算距離的變化量
Scale += scaleChange;			//將距離變化量的 1/10 當做縮放的比例
                break;
        }
    }
    BallPosition += Velocity * 
(float)gameTime.ElapsedGameTime.TotalSeconds;   //依據Flick操作的速度移動圖形
    Velocity *= 1f - (Friction * 
    (float)gameTime.ElapsedGameTime.TotalSeconds);//利用磨擦係數減緩圖形的移動速度
    float HalfWidth = (Ball.Width * Scale) / 2f;				//取得圖形寬度的 1/2
    float HalfHeight = (Ball.Height * Scale) / 2f;				//取得圖形高度的 1/2
    if (BallPosition.X < Window.ClientBounds.Left)		//判斷圖形是否觸及視窗的左邊界
    {
        BallPosition.X=Window.ClientBounds.Left;//設定圖形左上角點的X座標等於視窗左邊界
        Velocity.X *= -BounceMagnitude;               //設定反彈的速度為目前速度的一半
        HitWallEffect.Play();							  //播放碰撞視窗邊界的音效
    }
    if (BallPosition.X > Window.ClientBounds.Right – 
Ball.Width * Scale)					//判斷圖形是否觸及視窗的右邊界
    {
        BallPosition.X = Window.ClientBounds.Right – 
Ball.Width * Scale;			  //設定圖形右上角點的X座標等於視窗右邊界
        Velocity.X *= -BounceMagnitude;			   //設定反彈的速度為目前速度的一半
        HitWallEffect.Play();							  //播放碰撞視窗邊界的音效
    }
    if (BallPosition.Y < Window.ClientBounds.Top)		//判斷圖形是否觸及視窗的上邊界
    {
        BallPosition.Y = Window.ClientBounds.Top;//設定圖形左上角點的Y座標等於視窗上邊界
        Velocity.Y *= -BounceMagnitude;			    //設定反彈的速度為目前速度的一半
        HitWallEffect.Play();							  //播放碰撞視窗邊界的音效
    }
    if (BallPosition.Y > Window.ClientBounds.Bottom – 
Ball.Height * Scale)					//判斷圖形是否觸及視窗的下邊界
    {
        BallPosition.Y = Window.ClientBounds.Bottom – 
Ball.Height * Scale;			  //設定圖形右下角點的Y座標等於視窗下邊界
        Velocity.Y *= -BounceMagnitude;		        //設定反彈的速度為目前速度的一半
        HitWallEffect.Play();							  //播放碰撞視窗邊界的音效
    }
    base.Update(gameTime);
}

最后我们还要修改名称为 Draw 的方法,将游戏程序显示的图形显示在反应用户手势操作的位置上,做法如下:

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    // TODO: Add your drawing code here
    spriteBatch.Begin();									   //開始繪製遊戲內容
    if (isHold)											 //如果圖形被使用點中
    {
        spriteBatch.Draw(HoldBall, BallPosition, null, Color.White, 0, Vector2.Zero, 
Scale, SpriteEffects.None, 0);		//在指定的位置顯示中心有紅點的圓形
    }
    else
    {
        spriteBatch.Draw(Ball, BallPosition, null, Color.White, 0, Vector2.Zero, Scale, 
SpriteEffects.None, 0);					    //在指定的位置顯示圓形
    }
    spriteBatch.End();									   //結束繪製遊戲內容
    base.Draw(gameTime);
}

做好之后请执行 [建置] 项目的动作。

将设计妥的游戏程序部署到 Windows Phone 7 智能型手机

因为 Windows Phone 7 仿真器不支持用户触控操作,所以允许使用者利用手势进行游戏操作的游戏程序必须部署到真实的 Windows Phone 7 智能型手机才能测试和手势操作有关的功能是否正确,无法透过 Windows Phone 7 仿真器进行测试,因为在一般计算机上执行的 Windows Phone 7 仿真器无法接受用户透过手势动作进行操作。

[提示]

如果用户所使用的计算机配备的是触控屏幕,则在配备触控屏幕的计算机执行的 Windows Phone 7 仿真器仍然可以允许用户透过触控屏幕进行操作。

欲将开发妥的游戏程序部署到实际的 Windows Phone 7 智能型手机,而不是 Windows Phone 7 仿真器,程序设计师使用的计算机必须先安装Zune 软件。安装完成后请重新启动计算机,并将 Windows Phone 7 智能型手机连上开发计算机的 USB 端口,所安装的 Zune 软件将会在 Windows Phone 7 智能型手机连上计算机时自动启动。图1 所示即为 Zune 软件激活后的执行画面:

图1:Zune 执行的画面

Zune 自动启动之后,请执行 [所有程序 | Windows Phone Developer Tools] 群组中的 [Application Deployment] 程序,并在 [Application Deployment] 程序启动之后于 [Target] 下拉式选项中选择:Windows Phone 7 Device,再按下 [Browse] 键浏览到建置项目成功得到的 XAP 档案,如图2 所示:

图2:使用 [Application Deployment] 程序部署 Windows Phone 7 游戏程序的画面

做好之后按下 [Deploy] 键将所选择的 Windows Phone 7 游戏程序部署到 Windows Phone 7 智能型手机。请注意 Windows Phone 7 智能型手机不可处于屏幕锁定的状态,否则将无法部署成功。

[提示]

除了可以利用 [Application Deployment] 程序部署 Windows Phone 7 游戏程序到 Windows Phone 7 智能型手机以外,程序设计师也可以利用 Visual Studio 2010 Express for Windows Phone 直接将所开发的程序部署到 Windows Phone 7 智能型手机,不过程序设计师所使用的计算机仍然必须事先安装好 Zune 软件。程序设计师可以利用 [XNA Game Studio Device Management] 工具栏提供的下拉选项 [ ] 选择到 Windows Phone 7 Device,再按下 CTRL+F5 组合键将开发好的程序部署到 Windows Phone 7 智能型手机。同样地,Windows Phone 7 智能型手机不可以处于屏幕锁定的状态,否则将无法部署成功。

请执行部署到 Windows Phone 7 智能型手机的游戏程序,并利用 XNA Framework 支持的各种手势操作技巧点选、自由移动、轻拂、或放大缩小游戏程序显示的饼图案,体验利用手势操作程序的高度方便性。图3所示即为允许用户利用手势操作的程序执行的情形:

图3:允许用户利用手势操作的程序执行的情形

请注意当饼图案被轻拂至碰撞到程序的窗口时会发出声响并产生反弹。

程序下载:GestureControl.zip


PS:如果是Windows Phone8的真机解锁:

  WP8的解锁与WP7的不同。(主要是不用再理会Zune了)
    我们还是用“Windows Phone Developer Registration”进行解锁,但是可能在注册的时候会遇见“make sure the IpOverUsbSvc is running”的提示,我们只需打开“任务管理器”->“服务”->找到“IpOverUsbSvc”服务,先“停止”然后“开始”即可解决该问题。
    当然了,以后在调试的时候再也不用开启Zune神马的了,只要保证IpOverUsbSvc在运行即可。
    最后,解锁后的手机,可以通过 XapDeploy 工具直接部署Xap应用,在 X:\Program Files (x86)\Microsoft SDKs\Windows Phone\v8.0\Tools\XAP Deployment文件夹中即可找到。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值