Sprite Kit Manual Movement

There are two ways to make a sprite move in Sprite Kit. The first, which you might have noticed in the last chapter if you looked at the template code provided by Apple, is to use a concept calledactions. You’ll learn more about actions in the next chapter.


在Sprite Kit中有两种方法来处理sprite的移动.第一个,在苹果提供的模板代码中所使用的概念—actions.下一章的时候再讲…


The second way to make a sprite move is the more “classic” way – and that’s to set the position yourself manually over time.


第二种,比较传统的方式~随着时间不断的手动设置sprite的位置来使其移动.


It’s important to learn this way first,because it gives you the most control and helps you understand what actions do foryou.


考虑到其超强的可操作性,同时还可以帮助你理解actions究竟都做了什么,搞明白这种”笨”办法还是很有必要的~


However, in order to set a sprite’s position over time, you need to have a method that is called periodically as the game runs. This introduces a new topic – the Sprite Kit game loop.


无论如何,为了让sprite动起来,游戏运行的时候你都需要周期性的调用一个方法~这就引出了一个新的话题 — Sprite Kit 游戏循环.


The Sprite Kit game loop


A game works like a flipbook animation. You draw a successive sequence of images, and when you flip through them fast enough, it gives the illusion of movement.


游戏的工作原理类似翻图动画.画上一堆连续的图片,如果你快速的切换他们,就会有种动起来的效果.


Each individual picture that you draw is called a frame. Games typically try to draw frames between 30 to 60 times per second so that the animations feel smooth. This rate of drawing is called the frame rate, or specifically frames per second (FPS). By default, Sprite Kit shows you this in the bottom right corner of your game:


每一张单独的图片我们称之为一帧.通常来说,运行顺畅的游戏帧率在30到60之间(每秒帧数,FPS).默认情况下,SpriteKit会在屏幕的右下角显示当前的帧率


Note that you should only pay attention to the FPS display on an actual device, as you’ll get very different performance on the Simulator.


需要注意的是,你紧紧需要注意在真机测试时的帧率,因为其与模拟机的运行效率完全不同.


each frame, Sprite Kit does the following:


每显示一帧,SpriteKit都会执行以下操作


Calls a method on your scene called update:. This is where you can put code that you want to run every frame. It’s a perfect spot for code that updates the position or rotation of your sprites.


调用场景中一个叫做update:的方法,这里要编写的是你希望每一帧都运行的代码,非常适合更新sprite的位置和旋转角度.


2. Does some other stuff. You’ll revisit the game loop in other chapters.


一些其他的东西,之后会讲到


3. Renders the scene. Sprite Kit then draws all of the objects that are in your scene graph. Behind the scenes, this is issuing OpenGL draw commands for you.


演出开始!随后SpriteKit会将所有的元素画到场景上,底层则是通过OpenGL来帮助实现的.


Sprite Kit tries to draw frames as fast as possible, up to 60 FPS. However, note that if you take too long in your update: method, or if Sprite Kit has to draw more sprites at once than the hardware can handle, the frame rate might decrease.


SpriteKit会尝试用最快速的速度来绘制每一帧,最快可以到达60FPS.需要注意的是,如果你在update:方法中消耗了太多的时间,或者SpriteKit需要同时绘制的内容太多,帧率都会下降.


You’ll learn more about ways to resolve performance issues like this in Chapter 25, “Performance”, but for now you just need to know two things:


在第25章—性能中将会详细阐述提升效率的方法,但是就目前而言,记住这么两件事就够了:


Keep your update: method fast. For example, you want to avoid slow algorithms in this method since it’s called each frame.


保持update:方法的高效,举例来说,你会希望避免在绘制每一帧的时候调用运行速度过慢的算法.


2. Keep your count of nodes as low as possible. For example, it’s good to remove nodes from the scene graph when they’re off screen and you no longer need them.


尽量减少页面上的节点.举例来说,将移动到场景之外,而你有不在需要的节点删掉会是一个不错的选择.


Now that you know that update: is called each frame and is a good spot to update the position of your sprites, let’s make this zombie move!


现在你知道了update:方法会在绘制每一帧的时候调用,也就使其成为了调整sprite位置的最佳选择~接下来,该让僵尸动起来了!


Moving the zombie


To start, you’ll implement a simple but not ideal method: moving the zombie a fixed amount per frame.

我们将会以一个不那么理想的方法开场:每一帧让僵尸移动一个固定的距离.

每一帧移动固定距离


Inside MyScene.m, add the following method:


在MyScene.m文件中添加下列方法


- (void)update:(NSTimeInterval)currentTime {
	_zombie.position = CGPointMake(_zombie.position.x + 2, _zombie.position.y);
}


Here you update the position of the zombie to be two more points along the x-axis than last time, and keep the same position along the y-axis. This makes the zombie move from left to right.


这样,每一次僵尸会在x轴上向右移动两个点,而在y轴的位置不变,这让僵尸从左到右移动.


This is great stuff, but you might notice that the movement feels a bit jagged or stuttered. The reason for this goes back to the Sprite Kit game loop.


感觉不错,但你可能意识到了移动的过程有些许卡顿.原因在于SpriteKit的游戏循环.


Remember that Sprite Kit tries to draw frames as quickly as possible. However, there will usually be some variance in the amount of time it takes to draw each frame: sometimes a bit longer, sometimes a bit quicker.


SpriteKit会尝试用最快速度来绘制每一帧,但是通常来说,绘制每一帧的时间都不一样,有时长一点,有时短一点.


To see this yourself, add some code to print out how much time has elapsed since the last update. Add these variables to MyScene’s private variables section


通过添加一段代码可以看到这一区别,首先给MyScene添加两个私有变量:


	NSTimeInterval _lastUpdateTime; 
	NSTimeInterval _dt;


Then add these lines to the beginning of update:


然后在update:方法开始处添加如下代码:


if (_lastUpdateTime) {
  _dt = currentTime - _lastUpdateTime;
} else {
  _dt = 0;
}
_lastUpdateTime = currentTime;
NSLog(@"%0.2f milliseconds since last update", _dt * 1000);


Build and run, and you’ll see something like this in the console:


运行程序,就会看到如下输出内容:


ZombieConga[80642:70b] 0.00 milliseconds since last update 

ZombieConga[80642:70b] 23.37 milliseconds since last update 

ZombieConga[80642:70b] 16.88 milliseconds since last update 

ZombieConga[80642:70b] 16.90 milliseconds since last update 

ZombieConga[80642:70b] 16.17 milliseconds since last update 

ZombieConga[80642:70b] 17.28 milliseconds since last update


The correct solution is to figure out how fast you want the zombie to move per second, and then multiply this by the fraction of a second since the last update. Let’s give this a shot.


解决这个问题的办法是想清楚你希望僵尸每秒移动的距离,然后用它乘上上一次更新到现在的时间~放手一试~


通过时间增量来计算速率


Start by adding this constant to the top of the file, right after #import “MyScene.h”:


首先在#import “MyScene.h”下面添加一个常量:


	static const float ZOMBIE_MOVE_POINTS_PER_SEC = 120.0;


Here you’re saying that in one second, the zombie should move 120 points (about 1/5 of the screen).


这里确定了僵尸每秒钟移动120个点,大约是屏幕的1/5


Next, add a new private variable:


接下来添加一个私有变量:


	CGPoint _velocity;


So far you have seen CGPoints used to represent positions. However, it’s also quite common and handy to use CGPoints to represent 2D vectors instead.A 2D vector represents a direction and a length.


目前为止,我们知道了CGPoint可以用来表示一个位置.除此之外,它也经常用来表示二维矢量.一个二维矢量可以确定方向和距离


However, note that the velocity has no set position. After all, you should be able to make the zombie move in that direction, at that speed, no matter where the zombie starts.


毕竟速率不能指明方向.当这些搞定之后,无论僵尸在哪,你都可以控制僵尸按照指定的方向,速度来移动了.


Try this out by adding the following new method:


添加下边这段代码,然后试一下:


- (void)moveSprite:(SKSpriteNode *)sprite velocity:(CGPoint)velocity
{
    // 1
    CGPoint amountToMove = CGPointMake(velocity.x * _dt, velocity.y * _dt);
    NSLog(@"Amount to move: %@", NSStringFromCGPoint(amountToMove));
    // 2
    sprite.position = CGPointMake(sprite.position.x + amountToMove.x, sprite.position.y + amountToMove.y);
}


Here you’ve refactored the code into a reusable method that takes the sprite to be moved and a velocity vector by which to move it. Let’s go over this line-by-line:


这里我们把负责处理sprite移动的代码重成了一个可重用的方法,一行一行的来看一下:


1. Velocity is in points per second, and you need to figure out how much to move the zombie this frame. To determine that, this section multiplies the points per second by the fraction of seconds since the last update. You now have a point representing the zombie’s position (which you can also think of as a vector from the origin to the zombie’s position), and a vector representing the distance and direction to move the zombie this frame:


速率是每秒要移动的点数,你需要计算出当前的帧里僵尸需要移动的距离.用之前声明的每秒移动的点数乘以上次更新到现在所花的时间,就可以确定这个值.现在你有了在这一帧里僵尸的原点和移动僵尸的矢量


2. To determine the new position for the zombie, just add the vector to the point:


把矢量的值加到原点上就是僵尸的目标位置了


Finally, inside update:, replace the line that sets the zombie’s position with the following:


最后,把update:方法中设置僵尸位置的方法替换成下面的内容:


	[self moveSprite:_zombie velocity:CGPointMake(ZOMBIE_MOVE_POINTS_PER_SEC, 0)];


now the zombie will move much more smoothly across the screen.


现在僵尸移动变得平滑多了


If this still looks jittery to you, be sure to try it out on an actual device instead of on the Simulator, which has different performance characteristics.

如果现在看起来还是不够流畅,试试真机测试,会有不一样的性能表现.


向着触点移动


The goal is for the zombie to move toward where you tap, and keep going even after passing the tap, until you tap another location to draw his attention.


这一节的目标是让僵尸向着你在屏幕上点击的位置移动,即便过了这个点,僵尸的方向和速度依然不会改变,直到你再次触碰屏幕上的其他位置


There are four steps to make this work – let’s cover them one at a time.


我们分四步来实现这个功能,一个一个来~


Step 1: Find the offset vector

第一步:确定偏移量


First you need to figure out the offset between the location of the player’s tap and the location of the zombie. You can get this by simply subtracting the zombie’s position from the tap position.


首先,你需要确定僵尸位置到触点的偏移量,简单的用触点的位置减去僵尸位置即可.


Try this out by adding the following method:


添加这段代码:


- (void)moveZombieToward:(CGPoint)location {
	CGPoint offset = CGPointMake(location.x - _zombie.position.x, location.y - _zombie.position.y);
}


Step 2: Find the length of the offset vector

第二步:确定偏移的距离


Think of the offset vector as the hypotenuse of a right triangle, where the lengths of the other two sides of the triangle are defined by the x and y components of the vector:


把偏移量想想成为一个直角三角形的斜边,这样通过x和y就可以计算出偏移的距离了


Add the following line to the bottom of moveZombieToward:


在moveZombieToward:方法里面添加这段代码:


CGFloat length =
sqrtf(offset.x * offset.x + offset.y * offset.y);


Step 3: Make the offset vector a set length

第三步:把偏移量转化为固定的距离


Currently, you have an offset vector where:

• The direction of the vector points toward where the zombie should go.

The length of the vector is the length of the line between the zombie’s current position and where the player taps.


现在,你有了僵尸移动方向的向量和僵尸当前位置于用户触点之间的距离.

What you want is a velocity vector where:

• The direction points toward where the zombie should go.

• The length is ZOMBIE_MOVE_POINTS_PER_SEC (the constant you defined earlier – 120 points per second).


而我们需要的则是用来表示僵尸移动方向的点以及僵尸每秒需要移动的距离


So you’re halfway there – your vector points in the right direction, but isn’t the right length. How do you make a vector pointing in the same direction as the offset vector, but a certain length?


所以我们已经成功了一半了,你的向量点所表示的方向是正确的,但是距离却不正确.如何得到一个表示正确的方向与距离的点?


The first step is to convert the offset vector into a unit vector, which means a vector of length 1. According to geometry, you can do this by simply dividing the offset vector’s x and y components by the offset vector’s length.


第一步是把偏移向量转化成为单位向量,也就是说用1来表示一个完整单位的向量.根据几何学,用便宜向量的x和y除以向量的长度即可.


Once you have this unit vector, which you know is length 1, it’s easy to multiply it by ZOMBIE_MOVE_POINTS_PER_SEC to make it the exact length you want.


一旦你得到了单位向量,用它乘以ZOMBIE_MOVE_POINTS_PER_SEC就可以得到你希望的长度.


Give it a try. Add the following lines to the bottom of moveZombieToward:


添加下面的代码到moveZombieToward:方法试一下


CGPoint direction = CGPointMake(offset.x / length, offset.y / length);
_velocity = CGPointMake(direction.x * ZOMBIE_MOVE_POINTS_PER_SEC, direction.y * ZOMBIE_MOVE_POINTS_PER_SEC);


Step 4: Hook up to touch events

第四步:关联触摸事件


In Sprite Kit, to get notifications of touch events on a node, you simply need to set that node’s userInteractionEnabled property to YES and then override that node’s touchesBegan:, touchesMoved: and/or touchesEnded: methods. Unlike other SKNode objects, SKScene’s userInteractionEnabled property is set to YES by default.


在SpriteKit中,只需要简单的把userInteractionEnabled属性设置为YES,然后重写节点的touchesBegan:,touchesMoved:,以及touchesEnded:(其实还有一个touchesCancelld:)方法,就可以截获用户触摸事件.与其他SKNode不同的时,SKScene默认的userInteractionEnable就是YES.


To see it in action, implement these touch handling methods for MyScene as follows:


为MyScene实现下列的触摸响应方法,观察效果:


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 
    UITouch *touch = [touches anyObject];
    CGPoint touchLocation = [touch locationInNode:self.scene]; 
    [self moveZombieToward:touchLocation]; 
} 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 
    UITouch *touch = [touches anyObject];
    CGPoint touchLocation = [touch locationInNode:self.scene]; 
    [self moveZombieToward:touchLocation]; 
} 

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 
    UITouch *touch = [touches anyObject];
    CGPoint touchLocation = [touch locationInNode:self.scene]; 
    [self moveZombieToward:touchLocation]; 
} 

Finally, inside update:, edit the call to moveSprite: so that it passes in _velocity instead of the hardcoded amount:


最后更新以下update:方法,调用moveSprite:方法,这样就避免了在程序中使用硬编码的问题.


[self moveSprite:_zombie velocity:_velocity];


And now the zombie will chase toward where you tap. Just don’t get too close – he’s hungry!


现在僵尸就会朝着你的触点方向移动了.记住不要靠的太近 — 丫就一吃货!



边界检测


As you played the latest version of the game, you might have noticed that the zombie happily runs straight off the screen if you let him. While I admire his enthusiasm, in Zombie Conga you would like him to stay on the screen at all times, bouncing off an edge if he hits one.


在游戏的上一个版本中,你应该意识到了这个僵尸会屁颠屁颠的跑到屏幕外边去…好吧,我们对这个吃货的热情表示钦佩,但是在这个游戏里,我们希望它能始终待在屏幕里面,如果”撞到南墙”,那就该回头了.


The basic idea is this: you need to check if the newly calculated position is beyond any of the screen edges, and make the zombie bounce away if so. To do this, add this new method:


最基础的方案是这样的:如果在计算新的位置时发现他超出了屏幕范围,就应该把僵尸弹开.添加下面的方法来实现这个效果:


- (void)boundsCheckPlayer 
{
    // 1
    CGPoint newPosition = _zombie.position; CGPoint newVelocity = _velocity;
    // 2
    CGPoint bottomLeft = CGPointZero;
    CGPoint topRight = CGPointMake(self.size.width,self.size.height);
    // 3
    if (newPosition.x <= bottomLeft.x) 
    {
         newPosition.x = bottomLeft.x; newVelocity.x = -newVelocity.x;
    }

    if (newPosition.x >= topRight.x) 
    {
        newPosition.x = topRight.x; newVelocity.x = -newVelocity.x;
    }

    if (newPosition.y <= bottomLeft.y) 
    {
        newPosition.y = bottomLeft.y;
        newVelocity.y = -newVelocity.y; 
    }
    
    if (newPosition.y >= topRight.y) 
    {
        newPosition.y = topRight.y; newVelocity.y = -newVelocity.y;
    }
    // 4
    _zombie.position = newPosition;
    _velocity = newVelocity; 
}


Let’s go over this section-by-section:


一段一段来分析:


You store the position and velocity in variables. This is required because when you set the position property on a node, you can’t just set one component – you have to set the entire position in one shot,.For example, _zombie.position = CGPointMake(100, 100) is OK, but _zombie.position.x = 100 will result in a compiler error. Making a temporary CGPoint works around this.


我们把位置和速率属性保存在变量里.这是必须的,因为你在设置位置属性的时候不能够仅仅设置一个元素,你需设置整个positon.举例来说,_zombie.position = CGPointMake(100, 100)这是可以的,但是_zombie.position.x = 100就会产生一个编译器错误.


2. This gets the bottom left and top right coordinates of the screen.


这一步获取了屏幕的屏幕左下角和右上角的坐标.


3. Here you check the position to see if it’s beyond or at any of the screen edges. If it is, you clamp the position and reverse the appropriate velocity component to make the zombie bounce in the opposite direction.


这里则检查了僵尸的位置是否超过了屏幕的某一个边界.如果超过了,就让僵尸定在哪里,然后把适当的速率元素反转来把僵尸弹向相反的方向


4. You set the zombie to the new position.


把僵尸设置到新的位置上


Now call your new method at the end of update:


现在在update:方法的最后调用我们新添加的方法:


[self boundsCheckPlayer];


And now you have a zombie bouncing around the screen. I told you he was ready to party!


现在僵尸已经可以在屏幕里面玩弹球了~我早就说它已经准备好参加之后的派对了!


旋转僵尸


The zombie is moving nicely, but he always faces the same direction. Granted, he is undead, but this zombie is on the curious side and would like to turn to see where he’s going!


虽然僵尸已经移动的很好了,但头疼的是它始终面朝同一个方向.虽然他已经死了,但它依然对前方的道路充满好奇~


You already have a vector that includes the direction the zombie is facing: _velocity. You just need to get the angle to rotate so that the zombie faces in that direction.


你已经有了僵尸移动方向的向量:_velocity.只需要确定以下需要旋转的角度,僵尸就会朝向正确的方向了.


You may remember from trigonometry the mnemonic SOH CAH TOA, where the last part stands for:


你可能还记得那些三角定理(完全忘了),最后一部分是:


tan(angle) = opposite / adjacent

正切(角度) = 对边 / 邻边


Since you have the lengths of the opposite and adjacent sides, you can rewrite the

above formula as follows to get the angle of rotation:


既然我们已经有了对边和邻边的长度,我们可以简单的把上边的公式改写以下就可以得到需要旋转的角度了:


angle = arctan(opposite / adjacent)

角度 = 反正切(对边 / 邻边)


If none of this trigonometry rings any bells, don’t worry. Just think of it as a formula that you type in to get the angle – that’s all you need to know.


如果对这些东西完全不灵光…没事,知道通过这玩意儿能得到角度就行了...


Try it out by adding the following new method:


添加这么个方法试试:


- (void)rotateSprite:(SKSpriteNode *)sprite toFace:(CGPoint)direction
{
    sprite.zRotation = atan2f(direction.y, direction.x);
}


This just uses the equation from above. Note that this works because the zombie image is set up to be facing to the right. If the zombie were facing up instead, you’d have to add an additional rotation to compensate.


这里就是对上面公式的应用.需要注意的是,由于僵尸默认就面朝右边代码才能正常工作.如果僵尸一开始脸是朝上的,那就需要额外旋转一定角度.(怎么算就自己研究去吧…)


Now call this new method at the end of update:


现在,在update:方法的最后调用这个方法:


[self rotateSprite:_zombie toFace:_velocity];


And now the zombie rotates to face the direction in which he’s moving:


现在,僵尸就可以转向它移动的方向了.


Congratulations, you’ve given your zombie some life! The sprite moves smoothly, bounces off the edges of the screen and rotates – a great start to a game.


可喜可贺,你赋予了僵尸新生!我们的sprite移动的非常平滑,会被限制在屏幕范围只能,同时还会转身 — 漂亮的迈出了通向游戏世界的第一步!


But you’re not done yet – it’s time for you to try out some of this on your own to

make sure you’ve got this stuff down!


但是别高兴的太早,是你自己实现点什么东西来确保你真的学会这些了.


挑战


This chapter has three challenges, and they’re particularly important ones. Performing these challenges will give you useful practice with vector math and introduce some new math utilities you will be using throughout the rest of the book.


这一章有3个挑战,都很重要!搞定他们可以帮助你练习关于向量的数学知识,同时还将介绍一些剩下的章节中将要使用的数学工具.


Challenge 1: Math utilities

挑战1:数学工具


As you may have noticed while working on this game, you frequently have to perform calculations on points and vectors: adding and subtracting points, finding lengths, and so on.


在这款游戏的开发过程中,我们要对点和向量进行频繁的运算,加,减,求距离等等.


So far in this chapter, you’ve done this all yourself inline. That’s a fine way of doing things, but can get tedious and repetitive in practice. It’s also error-prone.


目前为止,这些都是我们通过编码手动实现的.这对于解决问题是非常不错的办法,但是对于这些枯燥乏味的工作难免感到厌倦,而且也容易写错.


Open MyScene.m and add the following functions to your file, right after #import “MyScene.h”:


打开MyScene.m文件,在#import “MyScene.h”下面添加这么几个函数


static inline CGPoint CGPointAdd(const CGPoint a, const CGPoint b)
{
    return CGPointMake(a.x + b.x, a.y + b.y);
}

static inline CGPoint CGPointSubtract(const CGPoint a,const CGPoint b)
{
    return CGPointMake(a.x - b.x, a.y - b.y);
}

static inline CGPoint CGPointMultiplyScalar(const CGPoint a, const CGFloat b)
{
    return CGPointMake(a.x * b, a.y * b);
}

static inline CGFloat CGPointLength(const CGPoint a) 
{
    return sqrtf(a.x * a.x + a.y * a.y); 
}

static inline CGPoint CGPointNormalize(const CGPoint a) 
{
    CGFloat length = CGPointLength(a);
    return CGPointMake(a.x / length, a.y / length); 
}

static inline CGFloat CGPointToAngle(const CGPoint a) 
{
    return atan2f(a.y, a.x); 
}


These are helper functions that implement many of the operations you’ve been working with already. For example, look at moveSprite:velocity::


这些都是对我们要处理的运算非常有用的方法,举个例子,看一下moveSprite:velocity:方法:


- (void)moveSprite:(SKSpriteNode *)sprite velocity:(CGPoint)velocity
{
    // 1
    CGPoint amountToMove = CGPointMake(velocity.x * _dt, velocity.y * _dt);
    NSLog(@"Amount to move: %@", NSStringFromCGPoint(amountToMove));
    // 2
    _zombie.position =
    CGPointMake(_zombie.position.x + amountToMove.x,
    _zombie.position.y + amountToMove.y);
}


You could simplify the first line by calling CGPointMultiplyScalar, and you could simplify the second line by calling CGPointAdd.


你可以简单的调用CGPointMultiplyScalar来重写第一行,调用CGPointAdd来重写第二行


Your challenge is to modify the game to use these new routines, and verify that the game still works as expected.


这里你的挑战是用这些方法来重构你的游戏,并且要保证游戏仍然正常运转.


Challenge 2: Stop that zombie!

挑战2:让他停下来!


In Zombie Conga, when you tap the screen the zombie moves toward that point – but then continues to move beyond that point.


在Zombie Conga中,当你点击了屏幕之后,僵尸会一直朝着那个点移动,但是当到达了那里之后它仍然会继续向前走.


That is the behavior you want for Zombie Conga, but in some games you might want the zombie to stop where you tap. Your challenge is to modify the game to do this.


这是在这款游戏中我们所需要的效果,但是有些游戏中,你可能希望僵尸停在你点击的地方.你的挑战就是实现这一效果.


Challenge 3: Smooth moves

挑战3:平滑的动作

Currently, the zombie immediately rotates to face where you tap. This can be a bit jarring – it would be nicer if the zombie would smoothly rotate over time to face the new direction.


现在你的僵尸会立即转向你点击的位置.这样就略显突兀了,如果能够慢慢的转向那个方向会好很多.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值