第三章 用户输入和碰撞检测

虽然看到一组漂亮的旋转圆环由您自己实现的时候是很酷的事情,但您还有漫漫长路要走。

尽管圆环动画看起来很漂亮,但是它没有干任何事情,并且它并不受您的控制。一个和玩家没有交互的游戏有什么乐趣可言呢?在这一章里,我们会研究用户输入和碰撞检测来使您的游戏除了看起来很漂亮外再干点有意义的事情。

这一章将会用到第2章末尾编写的代码(圆环动画),本章讨论到的知识点将在那个项目的基础上进行实践。

更多的精灵

如果您想要有一个受用户控制的物体并且加入和其他物体之间的碰撞检测,您至少还需要一个物体。让我们加入另一个精灵动画到项目中。

我们为第二个精灵动画使用一个不同的图像。在随书代码中的Chapter2/AnimatedSprites /AnimatedSprites /Content/Images文件夹下您可以找到一个名为skullball.Png(印有头骨图像的球)的图像文件。像以前一样,添加图片到项目中 (右键点击解决方案管理器中的Content/Image文件夹,选择添加->现有项,然后找到 skullball.Png并添加到项目中)。

接下来,您要添加一些用来绘制头骨图片并产生动画的变量。这些变量您看起来应该似曾相识了,因为它们和第2章中用来绘制圆环动画的变量很相似。在Game1类的顶部添加这些成员变量:

 

头骨图像帧的尺寸是75 X 75像素,在精灵图中有8行6列。 现在您应该改变绘制圆环动画用到的变量名以免产生混淆,为每个变量添加"rings"前缀,并修改所有的变量引用——这会帮助您使事情变得有条理。圆环动画相关的变量声明现在应该是这样:

 

 

编译项目确保重命名变量没有引入任何编译错误。如果有错误,记得变量名和之前项目中是一样的,只不过您刚给每个变量名添加了"rings"前缀。修正所有错误直到编译正确。

第4章将带您了解一些基本的面向对象设计原则来使精灵的添加变得更容易。不过目前您只是想添加一些用户输入和碰撞检测,所以让我们开始吧。

用之前一样的方法在LoadContent中将头骨图像加载到skullTexture变量中:

 

接下来添加在精灵帧序列中移动索引的代码。记住这是在Update方法中完成的。因为之前已经在圆环动画里做过这些,您可以直接将那段代码拷贝过来,然后将变量名修改一下就可以了:

 

最后,您要将精灵绘制到屏幕上,记住所有的绘制都在Draw方法中完成。同样的,您已经有了绘制圆环动画的代码,将它拷贝过来修改一下变量名。为了不让两 个精灵重叠在一起,修改头骨图像的Draw调用的第2个参数将图像绘制到(100,100)而不是(0,0),修改后的Draw方法看起来应该是这样:

 

现在编译运行程序,您会看到两幅图像都有动画了。如图3-1.

clip_image002

图3-1 两个干着自己的事的动画精灵

还不赖!在很短的时间里您就加入了一个全新的动画精灵到游戏中。软件开发时常带来令人兴奋和有成就感的时刻,不过在游戏开发中因为视觉感官(稍后还有听觉)的引入,使这种感觉放大了许多。这两个动画看起来相当酷,不过有趣的事才刚刚开始。现在您将要学习怎样控制屏幕上的物体并给您的程序提供一些用户交互。

XNA中的用户输入可以由一系列设备完成:键盘,鼠标,Xbox360控制器和Xbox360周边设备。Zune模拟其他的设备,比如Xbox360控制器,以允许您为这个平台的用户输入进行编码。鼠标输入在Xbox360上不可用,并且Zune只支持它的模拟控制——任何时候您都不会在Zune上插鼠标,键盘或其他设备。

在这一章里,您将为游戏添加对键盘,鼠标和Xbox360控制器的支持(译注:本书中交替使用控制器(Controller)和手柄(Gamepad)两个词,在本书两者含义相同)。

在之前的章节中,我们讨论了轮询和事件注册。两者之间的不同在处理输入设备的时候最显而易见。传统的Windows程序员习惯对事件进行注册,比如某个键被按下或鼠标移动。在这个编程模型中,应用程序执行一些功能,然后当它空闲时,消息泵将消息送入应用程序并且事件被处理。

在游戏开发中,没有所谓的空闲时间,所以允许开发者进行事件注册的开销过大。取而代之由开发者对输入设备进行轮询以得知玩家是否对这些设备进行了操作。

这是从一个稍微不同的角度看待输入和其他消息与事件,不过一旦您弄明白了,就会对游戏开发有更好的理解。

键盘输入

键盘输入通过Microsoft.XNA.Framework.Input命名空间中的Keyboard类来处理。Keyboard类有一个叫做GetState的静态方法用KeyboardState结构体的形式返回键盘目前的状态。

KeyboardState结构体中包含三个关键方法能够满足您大部分的功能需求,如表3-1所示:

表3-1 KeyboardState结构体中的关键方法

方法 描述

Keys[] GetPressedKeys()   返回一个在方法被调用时被按下的键的数组

bool IsKeyDown(Keys key)  返回true或false,取决于方法调用时参数所代表的按键是否

被按下。

bool IsKeyUp(Keys key)    返回true或false,取决于方法调用时参数所代表的按键是否

被释放。

———————————————————————————————————————

举个使用Keyboard类的例子,如果您想要检查"A"键是否被按下,使用如下代码:

 

 

在这个游戏中,您将修改代码允许用户用箭头键控制圆环精灵上下左右移动。

目前圆环精灵被硬编码绘制到(0,0),想要在屏幕上移动它,您要能够改变它绘制的位置。您需要一个Vector2变量来表示精灵当前的位置,还需要一个表示精灵移动速度的变量。因为在游戏过程中精灵移动的速度不会变,所以您可以让它作为一个常量。在Game1类的顶部添加这些成员变量:

 

同样,确保您将绘制圆环动画的Draw调用的第2个参数替换成ringsPosition 。

现在,您需要添加代码来检查上,下,左或右键是否被按下,如果任何一个被按下,改变ringsPosition变量的值来移动精灵。

您应该把检测输入的代码放在什么地方呢?记住决定在什么地方放置逻辑到游戏循环中时,只有两个地方可以选择:Draw方法,用来绘制所有的物体,Update方法,用来做绘制之外的其他事情(维护分数,移动物体,进行碰撞检测,等等)。所以,只管将以下代码添加到Update方法的base.Update调用之前:

 

这段代码怎么样呢——难道一个if/else语句不比4个if语句更有效率吗?对,的确是更有效率。但是一个if/else语句让您在同一时间只能向一个方向移动,而用4个单独的if语句让您可以沿着斜方向移动(比如,组合上和左键输入)。

还要注意您只在Update方法中调用一次GetState方法,然后在if语句中重用这次调用的结果,而不是在每个if语句中调用GetState。这是因为GetState方法调用的开销相当大,用这种方法您可以减少调用这个方法的次数。

编译并运行程序。您应该可以在屏幕上移动圆环精灵了,如图3-2:

clip_image004

图3-2 看——圆环在移动

鼠标输入

XNA提供了一个和Keyboard类行为很相似的Mouse类来和鼠标进行交互。Mouse类也有一个GetState方法能以MouseState结构体的形式从鼠标返回数据。Mouse类还有另一个值得一提的方法:void SetPosition(int x, int y)。这个方法能——您猜对了——让您可以设置鼠标光标的位置。这个位置是相对于游戏窗口的左上角而言。MouseState有一些属性将会帮助您了解到当您调用GetState的时候鼠标在特定时间发生了什么。这些属性的细节见表3-2。

表3-2 MouseState结构体中重要的属性

属性 类型 描述

LeftButton         ButtonState       返回鼠标左键的状态

MiddleButton       ButtonState       返回鼠标中键的状态

RightButton        ButtonState       返回鼠标右键的状态

ScrollWheelValue   int               返回自游戏开始后鼠标滚轮滚动刻度的累加量。

要想知道滚轮滚动了多少,把当前帧的

ScrollWheelValue和上一帧的进行比较。

X                  int               返回鼠标光标相对于游戏窗口左上角的水平位置

                                     (坐标)。如果鼠标光标在游戏窗口的左侧,这个

值是负值;如果在游戏窗口右边,这个值大于游

戏窗口的宽度。

XButton1          ButtonState        返回某些鼠标上额外的按键的状态。

XButton2          ButtonState        返回某些鼠标上额外的按键的状态。

Y                 int                返回鼠标光标相对于游戏窗口左上角的垂直位置

                                     (坐标)。如果鼠标光标在游戏窗口的上方,这个

值是负值;如果在游戏窗口下方,这个值大于游

戏窗口的高度。

————————————————————————————————————————

您也许注意到了默认情况下当鼠标划过XNA游戏窗口时鼠标指针是隐藏的。如果您想在窗口中显示鼠标光标,只要设置Game类的IsMouseVisible属性就可以了。

不管鼠标光标是否可见,GetState调用返回的MouseState结构体都持有鼠标当前的状态。

让我们用鼠标的移动来控制圆环精灵的移动。留着前一部分添加的键盘控制代码不动,最后您可以用多种方式来控制精灵。

因为MouseState的X和Y属性告诉您当前鼠标指针的位置,您可以将精灵的位置设置成鼠标的当前位置。

不过,因为您允许玩家还可以使用键盘,您不能只是把精灵的位置设置成鼠标的位置。如果您这样做,不管玩家是否移动鼠标精灵都会一直停留在鼠标所在的位置。

为了确定鼠标是否被移动,在Game1类中添加一个成员变量:

1   MouseState preMouseState;

这个变量将会追踪上一帧的鼠标状态。您将使用preMouseState在每一帧和当前帧的鼠标状态做比较。如果X和/或Y属性的值不同,您就知道玩家移动了鼠标并且您可以移动精灵到新的鼠标位置。将以下代码添加到Update方法的base.Update方法之前:

 

这段代码将精灵移动到鼠标所在的位置,但只是在鼠标被移动的情况下才这么做。如果您现在编译并运行程序,您应该可以用键盘或鼠标来控制精灵的移动了。


游戏手柄输入

如果您在进行Windows游戏开发,您仍然能够为一个Xbox360控制器编程。您需要一个有线控制器或者花大约20美元购买一个Xbox360无线接收器,可以让您连接4个无线控制器到PC上。

clip_image006

如果您购买了无线控制器的充电套装,将附有一根连接线,不过并没有数据通过线缆传输,所以即使您插上它还是一个无线控制器。充电套装的线缆用来输送电流进行充电,仅此而已。

如同XNA为鼠标输入提供了一个Mouse类和为键盘输入提供了一个Keyboard类,它提供了一个GamePad类从Xbox360手柄获得输入。不错,GamePad类也有一个GetState方法,和其他设备类一样。关于标准我要说几句,Microsoft XNA Framework是--绝大部分是跨越大规模系统(这里指一个framework和api)的标准如何带来巨大益处的极好例子。大多数时候,您可以仅仅通过对象类型和相似的成员函数来使用对象(译注:作者的意思是像Mouse,Keyboard,GamePad这几个类,它们属于同一类,有相似/相同的成员函数,所以使用一种之后很容易掌握另外的几种)。这是XNA小组杰出设计的贡献——向他们致敬。

GamePadState的主要属性列在表3-3中。

表3-3 GamePadState结构的重要属性

属性 类型 描述

Buttons     GamePadButtons       返回一个结构体告知当前哪些按键被按下。每个按键

由一个ButtonState枚举来表示,用来确定按键是否

被按下。

DPad       GamePadDPad           返回一个结构体告知DPad(十字键)上的哪个键被按

下。DPad结构体有4个按键(上,下,左,右),每

个按键由一个ButtonState表示,用来确定按键是否

被按下。

IsConnected boolean              指示控制器目前是否连接到Xbox360。

ThumbSticks GamePadThumbSticks   返回一个结构体用来确定模拟摇杆的方向。每个摇

杆(左边的和右边的)是一个X和Y成员取值范围为-1

到1的Vector2对象(拿左摇杆来说,如果您一直向

左推动,它的X值就为-1;如果您不动它,它的X

值就为0;如果您一直向右推,X值就为1)。

Triggers     GamePadTriggers     返回一个结构体告知扳机键是否被按下。Triggers

结构体包含两个浮点值(左和右)。值为0代表扳机键

没有被按下,值为1代表扳机键被按下。

———————————————————————————————————————

GamePadState结构体中包含两个能够满足您大部分需求的方法。方法列在表3-4.

表3-4 GamePadState结构体中的主要方法。

方法 描述

bool IsButtonDown(Buttons)              传递一个按键或用位或传递多个按键。

如果所有传入的按键被按下,返回true,否则

返回false。

bool IsButtonUp(Buttons)                 传递一个按键或用位或传递多个按键。

如果所有传入的按键被释放,返回true,否则

返回false。

———————————————————————————————————————

看看表3-3中的属性,您会注意到有些操作用布尔值或两态值表示(就是开或关),其它的由一个区间值表示(0到1,或-1到1)。 这些值和模拟控制器有关,因为它们不是简单的拥有开或关状态,它们在游戏中提供了更多的准确性和精确度。也许您注意到在Xbox360上的某些游戏里您可以使用扳机键和模拟摇杆以不同的速度移动——这是因为您按下某一个按键的特定方向,控制器会随着您按键的力度给程序发送不同强度的信号。这是一个为Xbox360控制器编码时应该记住的概念和一个应该加入到您游戏中的特色。在这部分里我们会讲到如何做到这些。

好了,让我们增加一些能让您用Xbox360手柄控制精灵的代码。和之前一样,留着鼠标和键盘控制代码不变,那么现在您有三种方式来控制您的精灵。

因为模拟摇杆能包含在-1和1范围内不同的X和Y值,您应该用ThumbSticks的属性值乘以ringsSpeed变量。那样当模拟摇杆一直朝一个方向按下时,精灵将会往那个方向全速移动;如果模拟摇杆只是稍微的推向一个方向,精灵以较慢的速度往那个方向移动。

以下的代码将依据控制器1上的左模拟摇杆按下的力度和方向来调整精灵的位置。将代码添加到Update方法中,就在键盘和鼠标输入处理代码之下:

 

编译并运行程序,您可以用Xbox360控制器对圆环精灵进行完全控制了。

让我们将事情变得有趣些。使用Xbox360控制器将会获得更多乐趣。让我们添加一个加速功能来使精灵的移动速度达到两倍。当然,加速模式下精灵在屏幕迅速移动的时候,您应该由于精灵的狂飙而从控制器感觉到震动。您或许之前在Xbox360控制器上感受过震动。这种机制被称为力回馈,它能大大的提高游戏体验,因为它甚至增加了另一种感官来使玩家有代入感。

SetVibration方法可以设置控制器震动马达的速度。这个方法返回一个布尔值用来指示调用是否成功(false意味着控制器没有连接或有一些其他问题)。这个方法接受一个玩家编号(译注:指控制器1~4)和一个为控制器马达设置的float值(0到1)。将这个值设为0时停止马达的震动。任何大于0的值都会使控制器以不同的速度震动。修改之前添加的用Xbox360控制器移动精灵的代码:

 

这段代码首先检查控制器上的A键是否被按下。如果是,加速模式被激活,您将会以正常速度的两倍来移动精灵并开启震动。如果A键被释放,将取消震动和加速。

编译并运行程序来实际感受一下它的运作方式。

如您所见,游戏手柄为输入增加了一种方式并相对于游戏本身提供了另一种不同的体验。它是一个很有用的工具,但并不是适合所有的游戏类型。确保您考虑好哪种输入方式对于您的游戏来说是最好的选择,因为输入方法对决定您游戏的娱乐性大有裨益。

保持精灵在游戏窗口中

您应该已经注意到如果您移动得足够远,精灵会在屏幕边缘消失。让玩家控制的物体能离开屏幕消失不见永远不是个好主意。要修正这一点,在Update方法的最后更新精灵的位置。如果精灵移动到窗口的上/下/左/右边缘之外,那么修正它的位置来保持它在游戏窗口中。在Update方法的base.Update调用之前加入以下代码:

 

编译并运行程序,您可以像以前一样在屏幕上移动精灵;不过它会一直保持在游戏窗口中而不会消失在屏幕外。

碰撞检测


到目前为止您干得很漂亮。玩家可以与您的游戏交互,在屏幕上移动精灵——但是还有一些要做的。您需要添加一些碰撞检测来更进一步。

碰撞检测是几乎任何游戏中都很关键的一个部分。您有玩过一个射击游戏在您击中目标后什么都没有发生吗?或一个赛车游戏中您看起来离围墙很远但是却撞上去了?这种游戏体验会惹恼玩家,导致这种结果的是糟糕的碰撞检测实现。

碰撞检测能增强或破坏游戏体验。之所以它是这么一个矛盾的问题,是因为您的碰撞检测算法越精准,游戏就变得越慢。对于碰撞检测很明显需要在精确性和效率之间做个权衡。一种最简单和快速的方法来实现碰撞检测是通过包围盒(bounding-box)算法。本质上当用一个包围盒算法时,您在屏幕上的每个物体周围“画”一个盒子,然后检查这些盒子是否相交。如果相交,就有碰撞。图3-3显示了圆环和头骨两个精灵周围隐藏的包围盒。

clip_image008

图3-3 两个精灵周围的包围盒

要在目前的游戏中实现包围盒算法,您需要为每个精灵创建一个基于精灵位置和精灵帧的宽和高的矩形。如果您将头骨精灵的位置改为变量会更容易理解代码。添加下列成员变量用来存放头骨精灵的位置。并且初始化变量为您现在绘制头骨精灵的位置(100,100)。

接下来,用这个变量取代绘制头骨精灵的Draw调用的第二个参数。

好,现在您有了一个变量来代表头骨精灵的位置,您可以用这个变量和精灵帧的尺寸创建一个矩形来检查它是否和用类似方法创建的圆环精灵的矩形相交。

添加下面的方法到Game1类中,它可以用XNA框架的Rectangle结构体为每个精灵创建一个包围矩形。结构体有一个叫做Intersects的方法可以用来检测两个矩形是否相交:

 

接下来您需要用这个Collide方法来检测两个物体是否发生碰撞。如果是,您应该执行一些操作。眼下您只是在精灵发生碰撞时调用Exit方法关闭游戏。显然,在真实游戏中您不会这么做,因为仅仅发生了类似碰撞这样的事情游戏就退出了对玩家而言像是一个Bug。

添加以下代码到Update方法中的base.Update之前:

 

编译并运行游戏,如果您移动圆环到太靠近头骨的位置,程序将会关闭。

您可能注意到圆环和头骨并没有真正的接触。为什么会这样呢?如果您仔细看看精灵图(图3-4),您会看到每个图像帧之间有些间距。这个距离在最大的圆环水平旋转的时候会变得更大(仔细看运行时的程序),空白也被作为碰撞的有效区域,这是因为您使用的帧尺寸而不是物体(圆环)真实的尺寸来创建包围矩形。

clip_image010
图3-4 在使用帧尺寸进行碰撞检测时图像中的空白会产生低精度的碰撞检测结果

一种修正这种情况的方法是调整精灵图以减少空白。另一种方法是创建一个小一号矩形来进行碰撞检测。这个小矩形必须以精灵为中心因此需要从每个帧边缘做少许偏移(译注:可以理解为从图3-3的样子缩小矩形以适应精灵的大小)。

clip_image012译者附:偏移指的是外围大矩形和里面的小矩形对应边之间的距离。

要创建一个小号的矩形,为每个精灵定义一个偏移变量,标明在每个方向上包围矩形要比精灵帧小多少(偏移多少)。增加以下两个变量到项目中:

 

下面,您将使用这些变量来构造一个比实际帧尺寸稍微小一点的矩形。修改Collide方法为下面这样,您会得到更精确的碰撞检测:

 

编译并运行程序试试新的碰撞检测效果。使用这个方法将会得到更精确的碰撞检测。有一个相近的算法用包围球来代替包围盒。您也可以用这个,特别当物体是圆形的时候,但是在将来的章节中会使用一些非圆形的物体,所以现在还是继续用包围盒好了。

尽管您已经使算法完善了一些,运行程序还是会发现碰撞检测不是100%的精确。在这个简单的测试中,不足显而易见。游戏中的目标并不需要100%精确的碰撞检测,而是达到一定程度上的精确使玩家感觉不到差别。

听起来像是作弊,但是实际上这归结为性能的问题。举例来说,假设您使用一个非圆形的精灵,像是飞机之类的。在飞机周围画一个单一的包围盒将产生非常不精确的碰撞检测结果。为避免这样您可以为飞机添加多个小的矩形然后用小矩形和游戏中的物体进行碰撞检测。这样的包围盒布局如图3-5所示:

clip_image014

图3-5 带一个包围和的飞机(左)和带多个包围和飞机(右)。

左边的例子会很不精确,而右边的能大幅提升算法的精确度,但是,您会遇到什么问题?假设您的游戏中有两架飞机并且您想看看它们是否发生碰撞。您现在需要将一 架飞机上的每个包围盒和另一架飞机上每个包围盒进行比较。两架飞机要进行25次比较计算!想象一下如果您加入更多的飞机到游戏中——计算量将会呈指数增长 并且最后影响您的游戏速度。

有一种办法靠两种算法相结合来提高性能。就是首先对整个物体的包围盒进行碰撞检测,如图3-5左边的例子。如果返回结果有潜在的碰撞可能(译注:就是说大的包围盒发生了碰撞),您可以继续深入对子包围盒进行碰撞检测,如图3-5右边的例子。

可以说碰撞检测的确是要在性能和精确度之前取得平衡。尽管有了诸多努力,使用了图3-5中右边例子中所有的包围盒进行碰撞检测,结果也不会是100%的精确。再次说明游戏中碰撞检测的目标是尽量精确而又不影响用户体验或性能。

还有一种加速碰撞检测的方法我应该提到。将一个游戏窗口划分成基于网格的坐标系让您可以做一个很简单的测试来确定两个物体是否离得足够近而需要进一步进行碰 撞检测。如果您追踪每个物体目前所在的格子,您可以在为两个物体运行碰撞检测算法之前通过检查确保这两个物体处于同一个格子中。这种方法可以在每帧中有效 的节省计算量并提升游戏的速度。

您刚刚做了什么

干的好!您有了一些很酷的动画,并且您可以在屏幕上移动精灵时进行碰撞检测,令人印象深刻。下面是对您在本章做的事情的概述:

•您实现了让用户通过键盘,鼠标和Xbox360手柄控制精灵。

•您用Xbox360控制器实现了力回馈。

•您实现了对两个动画精灵进行碰撞检测。

•您学习了怎样让碰撞检测在性能和精确度之间取得平衡。

摘要

•XNA支持的输入设备有键盘,
鼠标和Xbox360控制器。
•Xbox360有一些模拟输入允许每个按钮有不同的输入力度。
•碰撞检测始终是性能和精确度之间的平衡。通常算法的精确度越高,性能就越低。
•包围盒算法是最简单直接的碰撞检测算法中的一种。如果您在每个物体周围“画”一个假象的包围盒,您可以很容易知道哪个包围盒和另一个碰撞。
•您可以用不同的方法相结合来提高碰撞检测的精确度。用一个大的包围来检测是否值得花时间对物体各组成部分的包围盒进行碰撞检测,或者实现一个基于网格的系统来避免对没有互相靠近的物体进行多余的碰撞检测。
•在XNA中实现碰撞检测就像赢了波士顿马拉松赛一样——除了没有奔跑,没有流汗,没有波士顿,没有马拉松……

==============================================

知识测试:问答

1.从鼠标读取输入用哪个对象?

2.真还是假:XNA程序中读到的X,Y坐标表示鼠标自上一帧移动了多少。

3.模拟输入控制和数字输入控制有什么不同?

4.描述包围盒碰撞检测算法。

5.描述包围盒算法的优缺点。

6.在经典剧集Seinfeld(译注:中文译名《宋飞正传》)的某一集里,当George Costanza宣布他将以"T-Bone"为人所知时,Mr.Kruger为他起的什么名?

知识测试:练习

1. 让 我们把这一章和之前一章的一些元素组合起来。修改本章末尾的的代码使它包含另一个不受用户控制的精灵(使用plus.png图片,在 chapter0/AnimatedSprites/AnimatedSprites/Content/Image文件夹下)。让两个非用户控制精灵 (译 注:原本的头骨精灵和新添加的)都移动起来,像第二章里做的那样,使每个精灵都沿X,Y方向移动并在屏幕边缘反弹。为新的精灵也增加碰撞检测。结果您得到 了一个躲避移动精灵的游戏。当您碰到其他的精灵的时候,游戏结束。

为清楚起见,plus.png的帧尺寸是75 X 75像素,有4行6列(注意圆环和头骨的精灵图有8行6列)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值