PhysicsBody 理解

/****************************************************************************
 Copyright (c) 2013 Chukong Technologies Inc.
 
 http://www.cocos2d-x.org
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 ****************************************************************************/

#ifndef __CCPHYSICS_BODY_H__
#define __CCPHYSICS_BODY_H__

#include "ccConfig.h"
#if CC_USE_PHYSICS

#include "CCRef.h"
#include "CCGeometry.h"
#include "CCPhysicsShape.h"
#include "CCVector.h"

NS_CC_BEGIN

class Node;
class Sprite;
class PhysicsWorld;
class PhysicsJoint;
class PhysicsBodyInfo;

typedef Point Vect;


const PhysicsMaterial PHYSICSBODY_MATERIAL_DEFAULT(0.1f, 0.5f, 0.5f);

/**
 * 一个由物理因素构成的body, 它可以附上一个或多个形状
 * 你可以用createXXX创建一个body,它将用你指定的值(默认情况 PHYSICSBODY_MATERIAL_DEFAULT的默认密度为0.1)自动计算质量和力矩。

基本公式:mass = density * area.
 * 你可以用createEdgeXXX创建一个body,质量和力矩由默认值PHYSICS_INFINITY决定,它将是一个静态的body。
 * 你可以用setMass()改变质量,用setMoment()改变密度,用setDynamic()设置成一个静态的body。
 */
class PhysicsBody : public Ref
{
public:
    /** 用默认质量和力矩创建一个body */
    static PhysicsBody* create();
    /** mass 和 默认力矩,创建一个body */
    static PhysicsBody* create(float mass);
    /** mass 和 moment,创建一个body */
    static PhysicsBody* create(float mass, float moment);
    /**
 @brief:   创建一个圆形body
 radius:  半径
 material:  材质
 */
    static PhysicsBody* createCircle(float radius, const PhysicsMaterial& material = PHYSICSBODY_MATERIAL_DEFAULT, const Point& offset = Point::ZERO);
    /**
 @brief:   创建一个方形body
 size:   size(widht,height)
 */
    static PhysicsBody* createBox(const Size& size, const PhysicsMaterial& material = PHYSICSBODY_MATERIAL_DEFAULT, const Point& offset = Point::ZERO);
    /**
     * @brief:  创建一个多边形的body
     * points:  point数组,按顺时针方向绕成的封闭图形。
     */
    static PhysicsBody* createPolygon(const Point* points, int count, const PhysicsMaterial& material = PHYSICSBODY_MATERIAL_DEFAULT, const Point& offset = Point::ZERO);
   
    /**
 @brief:   创建一个静态的线段
 border:  边缘宽度
 */
    static PhysicsBody* createEdgeSegment(const Point& a, const Point& b, const PhysicsMaterial& material = PHYSICSBODY_MATERIAL_DEFAULT, float border = 1);
    /**
 @brief:   创建一个静态的方形
 */
    static PhysicsBody* createEdgeBox(const Size& size, const PhysicsMaterial& material = PHYSICSBODY_MATERIAL_DEFAULT, float border = 1, const Point& offset = Point::ZERO);
    /**
 @brief:   创建一个静态的多边形
 */
    static PhysicsBody* createEdgePolygon(const Point* points, int count, const PhysicsMaterial& material = PHYSICSBODY_MATERIAL_DEFAULT, float border = 1);
    /**
 @brief:   创建一个静态的链条形
 */
    static PhysicsBody* createEdgeChain(const Point* points, int count, const PhysicsMaterial& material = PHYSICSBODY_MATERIAL_DEFAULT, float border = 1);
   
    /*
     * @brief 添加一个形状到body
     * @param shape 添加的形状
     * @param addMassAndMoment == true,添加形状的质量个力矩将被添加,默认为true。
     */
    virtual PhysicsShape* addShape(PhysicsShape* shape, bool addMassAndMoment = true);
    /*
     * @brief 从body移除一个形状
     * @param shape 移除的形状
     * @param addMassAndMoment == true,移除形状的质量个力矩将被移除,默认为true。
     */
    void removeShape(PhysicsShape* shape, bool reduceMassAndMoment = true);
    /*
     * @brief 通过tag标记移除一个形状
     */
    void removeShape(int tag, bool reduceMassAndMoment = true);
    /* 移除所有形状 */
    void removeAllShapes(bool reduceMassAndMoment = true);
    /* 获取body的所有形状 */
    inline const Vector<PhysicsShape*>& getShapes() const { return _shapes; }
    /* 获取body的第一个形状 */
    inline PhysicsShape* getFirstShape() const { return _shapes.size() >= 1 ? _shapes.at(0) : nullptr; }
    /* 通过tag标记获取一个形状 */
    PhysicsShape* getShape(int tag) const;
   
    /**
 @brief:   给body施加一个力,循序渐进——ApplyForce
 force:   动力方向上的一个向量。
 */
    virtual void applyForce(const Vect& force);
    /**
 @brief:   给body施加一个力,循序渐进——ApplyForce
 force:   动力方向上的一个向量。
 offset:  偏移度,值越大 旋转越快 偏移角度越大
 */
    virtual void applyForce(const Vect& force, const Point& offset);
    /** 重置所有应用到body上的力 */
    virtual void resetForces();
 /**
 @brief:   给body施加一个力,即时叠加——applyImpulse
 */
    virtual void applyImpulse(const Vect& impulse);
    /** Applies a continuous force to body. */
    virtual void applyImpulse(const Vect& impulse, const Point& offset);
    /**
 @brief:   给body施加一个力,朝向设定点角度方向的力矩,使其旋转。
 */
    virtual void applyTorque(float torque);
   
    /** 设置速度 */
    virtual void setVelocity(const Vect& velocity);
    /** 获取速度 */
    virtual Point getVelocity();
    /** 设置角速度 */
    virtual void setAngularVelocity(float velocity);
    /** 设置角速度,相对于局部某一点 */
    virtual Point getVelocityAtLocalPoint(const Point& point);
    /** 设置角速度,相对于世界某一点 */
    virtual Point getVelocityAtWorldPoint(const Point& point);
    /** 获取角速度 */
    virtual float getAngularVelocity();
    /** 设置速度的最大值 */
    virtual void setVelocityLimit(float limit);
    /** 获取速度的最大值 */
    virtual float getVelocityLimit();
    /** 设置角速度的最大值 */
    virtual void setAngularVelocityLimit(float limit);
    /** 获取角速度的最大值 */
    virtual float getAngularVelocityLimit();
   
    /** 将body从它所加入的物理世界移除 */
    void removeFromWorld();
   
    /** 获取body所在的物理世界 */
    inline PhysicsWorld* getWorld() const { return _world; }
    /** 获取所有关节 */
    inline const std::vector<PhysicsJoint*>& getJoints() const { return _joints; }
   
    /** 获取body所设置的精灵 */
    inline Node* getNode() const { return _node; }
   
    /**
  * 设置类别掩码
  * 用1bit掩码位来标记,物理body所属的一个类别。
  * 每一个物理body在场景中能被分配到多达32个不同的类别,它们分别对应一个比特的比特掩码位。你可以定义你在游戏中使用的掩码值。
  * 当您定义物理body相互作用时,你的游戏通知这些相互作用, 属性collisionBitMask和属性contactTestBitMask可以结合使用。
     * 默认值是0xFFFFFFFF,所有位都设置。
     */
    void setCategoryBitmask(int bitmask);
    /**
  * 设置接触掩码
  * 当两个body共享相同的空间时,每个body的类别掩码对其他body的接触掩码执行逻辑与。
  * 如果任意比较结果为非零值,PhysicsContact对象被创建并且传递到物理世界的委托。
     * 为获得最佳的性能,我仅仅设置我们感兴趣接触掩码位。
  * 默认值是0x00000000(所有位为空)。
     */
    void setContactTestBitmask(int bitmask);
    /**
  * 设置碰撞掩码
  * 当两个body相互接触,发生碰撞时。该body的碰撞掩码与另一个body的类别掩码执行逻辑与操作。
  * 如果结果为非零值,该body将受到另一个body的碰撞。
  * 每个body都可以独立的选择,他们是否受到其他body的影响。
  * 例如:你可以用这一点来进行避免碰撞计算,这将使你忽略掉速度的改变。
  * 默认值为0xFFFFFFFF,(所有位都设置)
     */
    void setCollisionBitmask(int bitmask);
    /** get the category bit mask */
    inline int getCategoryBitmask() const { return _categoryBitmask; }
    /** get the contact test bit mask */
    inline int getContactTestBitmask() const { return _contactTestBitmask; }
    /** get the collision bit mask */
    inline int getCollisionBitmask() const { return _collisionBitmask; }
   
    /**
     * 设置body组。
  * 碰撞组让你指定一个完整的组下标。你可以让同一个组下标的所有body都接受碰撞或者都不接收碰撞。
     * 它比位掩码优先级更高。
     */
    void setGroup(int group);
    /** 获取body属于哪个组 */
    inline int getGroup() const { return _group; }
   
    /** 获取body位置 */
    Point getPosition() const;
    /** 获取body角度 */
    float getRotation() const;
   
    /** 设置body的偏移量, 它是相对于节点的位置 */
    void setPositionOffset(const Point& position);
    /** get body position offset. */
    Point getPositionOffset() const;
    /** 获取偏移量 */
    void setRotationOffset(float rotation);
    /** 设置旋转偏移量 */
    float getRotationOffset() const;
   
    /**
     * @brief body是否为动态的。
     */
    inline bool isDynamic() const { return _dynamic; }
    /**
     * @brief 设置body,true为动态,false为静态。就是不动。
     */
    void setDynamic(bool dynamic);
   
    /**
     * @brief 设置body的质量。
  * 备注:如果你需要给body增加或者减少质量,不要使用setMass(getMass() +/- mass),
  * 因为body的质量可能等于PHYSICS_INFINITY,那么它将引起一些不可意料的结果,请用addMass()代替。
     */
    void setMass(float mass);
    /** 获取body质量 */
    inline float getMass() const { return _mass; }
    /**
     * @brief 增加质量
  * 如果 _mass == PHYSICS_INFINITY,那么body质量不变;
  * 如果 mass == PHYSICS_INFINITY,那么body质量为PHYSICS_INFINITY;
  * 如果 mass == -PHYSICS_INFINITY,那么body质量将不会改变;
  * 如果 mass + _mass <= 0,那么body的质量将会等于MASS_DEFAULT(1.0);
  * 其他情况mass = mass + _mass;
     */
    void addMass(float mass);
   
    /**
     * @brief 设置body力矩,
  * 备注:同设置质量一样。
 */
    void setMoment(float moment);
    /** 获取body力矩 */
    inline float getMoment() const { return _moment; }
    /**
     * @brief 添加body力矩
     */
    void addMoment(float moment);
    /** 获取线性阻力 */
    inline float getLinearDamping() const { return _linearDamping; }
    /**
     * 设置线性阻力
     * 它被用来模拟body与流体或空气的阻力。
  * 它的值在0.0f 到 1.0f之间。
     */
    inline void setLinearDamping(float damping) { _linearDamping = damping; updateDamping(); }
    /** 获取角阻力 */
    inline float getAngularDamping() const { return _angularDamping; }
    /** 设置角阻力 */
    inline void setAngularDamping(float damping) { _angularDamping = damping; updateDamping(); }
   
    /** body是否为休息状态 */
    bool isResting() const;
    /** set body to rest */
    void setResting(bool rest) const;
    /**
     * body是否可用
     */
    inline bool isEnabled() const { return _enable; }
    /**
     * set the enable value.
     * if the body it isn't enabled, it will not has simulation by world
     */
    void setEnable(bool enable);
   
    /** 角旋转是否可用 */
    inline bool isRotationEnabled() const { return _rotationEnable; }
    /** set the body is allow rotation or not */
    void setRotationEnable(bool enable);
   
    /** 是否受到物理世界的引力 */
    inline bool isGravityEnabled() const { return _gravityEnable; }
    /** set the body is affected by the physics world's gravitational force or not. */
    void setGravityEnable(bool enable);
   
    /** 获取body的tag标记 */
    inline int getTag() const { return _tag; }
    /** set the body's tag */
    inline void setTag(int tag) { _tag = tag; }
   
    /** 世界坐标转换成本地坐标 */
    Point world2Local(const Point& point);
    /** 本地坐标转换成世界坐标 */
    Point local2World(const Point& point);
   
protected:
   
    bool init();
   
    virtual void setPosition(Point position);
    virtual void setRotation(float rotation);
   
    void update(float delta);
   
    void removeJoint(PhysicsJoint* joint);
    inline void updateDamping() { _isDamping = _linearDamping != 0.0f ||  _angularDamping != 0.0f; }
   
protected:
    PhysicsBody();
    virtual ~PhysicsBody();
   
protected:
    Node* _node;
    std::vector<PhysicsJoint*> _joints;
    Vector<PhysicsShape*> _shapes;
    PhysicsWorld* _world;
    PhysicsBodyInfo* _info;
    bool _dynamic;
    bool _enable;
    bool _rotationEnable;
    bool _gravityEnable;
    bool _massDefault;
    bool _momentDefault;
    float _mass;
    float _area;
    float _density;
    float _moment;
    bool _isDamping;
    float _linearDamping;
    float _angularDamping;
    int _tag;
   
    int _categoryBitmask;
    int _collisionBitmask;
    int _contactTestBitmask;
    int _group;
   
    bool _positionResetTag;     /// 为了避免body位置复位的时候调用 Node::setPosition().
    bool _rotationResetTag;     /// 为了避免body位置复位的时候调用 Node::setPosition().
    Point _positionOffset;
    float _rotationOffset;
   
    friend class PhysicsWorld;
    friend class PhysicsShape;
    friend class PhysicsJoint;
    friend class Node;
};

NS_CC_END

#endif // CC_USE_PHYSICS
#endif // __CCPHYSICS_BODY_H__

在跑酷熊猫游戏中,我们已经实现了熊猫在一条无限循环的道路上奔跑,并且可以通过左右滑动来避开障碍物。现在我们要添加更多的游戏元素,使游戏更加有趣。 ## 1. 道路随机生成 我们要让游戏中的道路不再是一条固定的直线,而是随机生成的。我们可以通过几个步骤来实现这个功能: ### 1.1 创建地图 我们先创建一个地图类,用来表示游戏中的道路。地图由多个段组成,每个段包含一个随机生成的道路和一些障碍物。 ```swift class Map { var segments = [Segment]() init() { generateSegments() } func generateSegments() { // 生成一些段,每个段包含一个随机生成的道路和一些障碍物 } } ``` ### 1.2 随机生成道路 我们可以定义一个 `Road` 结构体,表示一段道路。道路由多个点组成,每个点包含一个 x 坐标和一个 y 坐标。 ```swift struct Road { var points = [CGPoint]() } ``` 在 `Map` 类中,我们可以实现一个方法来随机生成一段道路。我们可以先生成一些控制点,然后通过这些控制点计算出道路上的点。 ```swift func generateRoad() -> Road { let startX = CGFloat.random(in: -100...100) let startY = CGFloat.random(in: -100...100) let endX = CGFloat.random(in: -100...100) let endY = CGFloat.random(in: -100...100) let controlX1 = CGFloat.random(in: -100...100) let controlY1 = CGFloat.random(in: -100...100) let controlX2 = CGFloat.random(in: -100...100) let controlY2 = CGFloat.random(in: -100...100) let tValues = stride(from: 0, through: 1, by: 0.01) let points = tValues.map { t -> CGPoint in let x = pow(1-t, 3) * startX + 3 * pow(1-t, 2) * t * controlX1 + 3 * (1-t) * pow(t, 2) * controlX2 + pow(t, 3) * endX let y = pow(1-t, 3) * startY + 3 * pow(1-t, 2) * t * controlY1 + 3 * (1-t) * pow(t, 2) * controlY2 + pow(t, 3) * endY return CGPoint(x: x, y: y) } return Road(points: points) } ``` 这个方法会生成一个随机的起点和终点,以及两个控制点。然后使用贝塞尔曲线计算出道路上的点,并返回一个 `Road` 结构体。 ### 1.3 随机生成障碍物 在每个段中,我们还需要随机生成一些障碍物。我们可以定义一个 `Obstacle` 结构体,表示一个障碍物。障碍物由多个点组成,每个点包含一个 x 坐标和一个 y 坐标。 ```swift struct Obstacle { var points = [CGPoint]() } ``` 在 `Map` 类中,我们可以实现一个方法来随机生成一些障碍物。我们可以先生成一些控制点,然后通过这些控制点计算出障碍物上的点。 ```swift func generateObstacle() -> Obstacle { let startX = CGFloat.random(in: -50...50) let startY = CGFloat.random(in: -50...50) let endX = CGFloat.random(in: -50...50) let endY = CGFloat.random(in: -50...50) let controlX1 = CGFloat.random(in: -50...50) let controlY1 = CGFloat.random(in: -50...50) let controlX2 = CGFloat.random(in: -50...50) let controlY2 = CGFloat.random(in: -50...50) let tValues = stride(from: 0, through: 1, by: 0.01) let points = tValues.map { t -> CGPoint in let x = pow(1-t, 3) * startX + 3 * pow(1-t, 2) * t * controlX1 + 3 * (1-t) * pow(t, 2) * controlX2 + pow(t, 3) * endX let y = pow(1-t, 3) * startY + 3 * pow(1-t, 2) * t * controlY1 + 3 * (1-t) * pow(t, 2) * controlY2 + pow(t, 3) * endY return CGPoint(x: x, y: y) } return Obstacle(points: points) } ``` 这个方法会生成一个随机的起点和终点,以及两个控制点。然后使用贝塞尔曲线计算出障碍物上的点,并返回一个 `Obstacle` 结构体。 ### 1.4 生成多个段 有了上面两个方法,我们就可以在 `Map` 类中实现一个方法来生成多个段了。每个段包含一个随机生成的道路和一些障碍物。我们可以随机生成一些段,然后将这些段添加到 `segments` 数组中。 ```swift func generateSegments() { segments.removeAll() var lastEnd = CGPoint.zero for _ in 0..<10 { let road = generateRoad() let obstacleCount = Int.random(in: 1...3) var obstacles = [Obstacle]() for _ in 0..<obstacleCount { obstacles.append(generateObstacle()) } let segment = Segment(road: road, obstacles: obstacles, start: lastEnd) segments.append(segment) lastEnd = road.points.last! } } ``` 这个方法会生成 10 个随机的段,并将它们添加到 `segments` 数组中。每个段的起点都和上一个段的终点相连,形成一条无限循环的道路。 ### 1.5 显示地图 现在我们已经生成了一个随机的地图,接下来要将它显示在游戏中。我们可以在 `GameScene` 类中添加一个 `map` 属性,并在 `didMove(to:)` 方法中创建这个地图。 ```swift class GameScene: SKScene { var panda: Panda! var map: Map! override func didMove(to view: SKView) { panda = Panda() addChild(panda) map = Map() for segment in map.segments { let roadNode = SKShapeNode(splinePoints: segment.road.points, count: segment.road.points.count) roadNode.strokeColor = .white roadNode.lineWidth = 3 addChild(roadNode) for obstacle in segment.obstacles { let obstacleNode = SKShapeNode(splinePoints: obstacle.points, count: obstacle.points.count) obstacleNode.strokeColor = .red obstacleNode.lineWidth = 3 addChild(obstacleNode) } } } } ``` 在 `didMove(to:)` 方法中,我们先创建一个熊猫节点,并将它添加到场景中。然后创建一个地图对象,遍历地图中的每个段,创建一个道路节点和一些障碍物节点,并将它们添加到场景中。 现在运行游戏,就会看到一条随机生成的道路和一些随机生成的障碍物了! ## 2. 碰撞检测 我们已经随机生成了一些障碍物,现在要实现碰撞检测,当熊猫碰到障碍物时,游戏就结束。 ### 2.1 添加物理引擎 要进行碰撞检测,我们需要使用 SpriteKit 中的物理引擎。在 `GameScene` 类中,我们可以添加一个物理世界,并将熊猫和障碍物添加到这个世界中。 ```swift class GameScene: SKScene { var panda: Panda! var map: Map! var world: SKPhysicsWorld! override func didMove(to view: SKView) { panda = Panda() addChild(panda) map = Map() for segment in map.segments { let roadNode = SKShapeNode(splinePoints: segment.road.points, count: segment.road.points.count) roadNode.strokeColor = .white roadNode.lineWidth = 3 addChild(roadNode) for obstacle in segment.obstacles { let obstacleNode = SKShapeNode(splinePoints: obstacle.points, count: obstacle.points.count) obstacleNode.strokeColor = .red obstacleNode.lineWidth = 3 addChild(obstacleNode) let physicsBody = SKPhysicsBody(polygonFrom: obstacle.points) physicsBody.categoryBitMask = 1 physicsBody.contactTestBitMask = 2 obstacleNode.physicsBody = physicsBody } } physicsWorld.gravity = CGVector(dx: 0, dy: -9.8) physicsWorld.contactDelegate = self world = physicsWorld } } ``` 在 `didMove(to:)` 方法中,我们先创建一个熊猫节点,并将它添加到场景中。然后创建一个地图对象,遍历地图中的每个障碍物,创建一个障碍物节点,并将它添加到场景中。同时为障碍物添加一个物理体,并设置它的分类掩码和接触测试掩码。最后设置物理世界的重力和接触代理。 ### 2.2 检测碰撞 在 `GameScene` 类中,我们还需要实现 `SKPhysicsContactDelegate` 协议中的 `didBegin(_:)` 方法,检测熊猫和障碍物之间的碰撞。 ```swift class GameScene: SKScene, SKPhysicsContactDelegate { // ... func didBegin(_ contact: SKPhysicsContact) { if contact.bodyA.node == panda || contact.bodyB.node == panda { gameOver() } } func gameOver() { print("Game Over") // 处理游戏结束 } } ``` 在 `didBegin(_:)` 方法中,我们检查熊猫和障碍物是否发生了碰撞,如果发生了碰撞,就调用 `gameOver()` 方法,处理游戏结束的逻辑。 ### 2.3 开启碰撞检测 最后,我们还需要在 `Panda` 类中开启碰撞检测。 ```swift class Panda: SKSpriteNode { init() { // ... let physicsBody = SKPhysicsBody(texture: texture!, size: size) physicsBody.categoryBitMask = 2 physicsBody.contactTestBitMask = 1 physicsBody.collisionBitMask = 0 physicsBody.affectedByGravity = true physicsBody.allowsRotation = false physicsBody.velocity.dy = 200 physicsBody.angularVelocity = 0 physicsBody.linearDamping = 0 physicsBody.angularDamping = 0 physicsBody.restitution = 0 physicsBody.friction = 0 physicsBody.mass = 1 self.physicsBody = physicsBody } } ``` 在 `Panda` 类的初始化方法中,我们创建一个物理体,并设置它的分类掩码、接触测试掩码、碰撞掩码、重力、旋转等属性。最后将物理体添加到熊猫节点上。 现在运行游戏,当熊猫碰到障碍物时,游戏就会结束了! ## 3. 总结 在本章中,我们学习了如何让游戏中的道路随机生成,并实现了碰撞检测的功能。下一章中,我们将继续完善游戏,添加更多的功能和效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值