Android游戏开发–设计游戏实体–策略模式

在这一部分中,我将尝试解释我对好的游戏设计元素的理解。 我将在示例中使用droid,并编写基本的战斗模拟器脚本以查看其行为。

问题:

我指挥一个机器人,我想消灭敌人。 再次面对相同类型的敌人很无聊。 我需要新的挑战,这意味着需要新型的敌人。 例如,在第一级中,我只想练习目标。 所以我需要一个漂亮的傻瓜敌人,不做太多事情,但会射击。 掌握了该技能(射击无助的机器人)之后,我需要挑战,并且希望敌方机器人进行反击,但是由于我仍然是初学者,所以我不想很快死去,所以我需要较弱的机器人。 和他们在一起之后,我想面对一个更艰巨的挑战。 我需要更好更强的机器人。 不仅更强大,而且在行为上也有所不同,而且它可能会无聊地一遍又一遍地杀死相同类型的敌人。

显而易见的解决方案:

为3种类型的敌方机器人创建3个类别。 为了简单起见,每个机器人都有2种能力:移动和攻击。 使用这两种方法创建Droid接口并让每个droid实现它们都是有意义的。

他们可以移动 射击 。 好吧,不是全部,但是我们可以为不做任何事情的人提供空的实现。

机器人类型:

  • 诱饵机器人 -没有武器,无法移动。
  • 侦察员Droid –武器较弱且动作Swift。
  • 突击机器人 -将拥有重型武器并缓慢移动。

查看这三种类型,我们可以实现以下简单的类图:

该接口具有3种简单的方法,机器人需要实现这些方法:

public interface Droid {

	// display some info of the droid
	public void display();

	// move the droid
	public void move(int x, int y);

	// attack position
	public void shoot(int x, int y);
}

这三个类如下:

DecoyDroid.java

public class DecoyDroid implements Droid {

	@Override
	public void display() {
		System.out.println("I am a DECOY droid");
	}
		@Override
	public void move(int x, int y) {
		System.out.println("Can't move.");
	}

	@Override
	public void shoot(int x, int y) {
		System.out.println("Have no weapon.");
	}
}

ScoutDroid.java

public class ScoutDroid implements Droid {

	private float damage = 0.5f;

	@Override
	public void display() {
		System.out.println("I am a scout droid");
	}

	@Override
	public void move(int x, int y) {
		System.out.println("Moving QUICKLY to: " + x + "," + y + ".");
	}

	@Override
	public void shoot(int x, int y) {
		System.out.println("Light Laser Canon targeting: " + x + "," + y
				+ ". Damage: " + damage);
	}
}

AssaultDroid.java

public class AssaultDroid implements Droid {

	private float 	damage = 2.5f;
	private boolean loaded = true;

	@Override
	public void display() {
		System.out.println("I am an ASSAULT droid");
	}

	@Override
	public void move(int x, int y) {
		System.out.println("Moving SLOWLY to: " + x + "," + y + ".");
	}

	@Override
	public void shoot(int x, int y) {
		if (loaded) {
			System.out.println("Heavy laser targeting: " + x + "," + y
					+ ". Damage: " + damage);
			loaded = false;
		} else {
			System.out.println("Reloading...");
			loaded = true;
		}
	}
}

无论ScoutDroidAssaultDroid有争论的损害 。 这保留了他们造成的损害的价值。

为了给AssaultDroid带来重型武器并且重装时间很慢,我们添加了load变量。 这样,攻击机器人需要转两圈才能发射一次武器。

我为机器人创建了一个简单的模拟器来轮流移动和射击。
为此设计运行模拟器:

BadDroidSimulator.java

public class BadDroidSimulator {

	public static void main(String[] args) {
		// for generating random numbers
		Random rand = new Random();

		Droid scout = new ScoutDroid();
		Droid assailant = new AssaultDroid();
		Droid decoy = new DecoyDroid();

		scout.display();
		assailant.display();
		decoy.display();

		// shoot-out - each droid fires once per turn
		for (int i = 1; i <= 5; i++) {
			System.out.println("\n<=== BEGIN TURN " + i + " ===>");
			scout.shoot(rand.nextInt(10), rand.nextInt(10));	// we assume this is an enemy position
			scout.move(rand.nextInt(10), rand.nextInt(10));
			System.out.println();
			assailant.shoot(rand.nextInt(10), rand.nextInt(10));
			assailant.move(rand.nextInt(10), rand.nextInt(10));
			System.out.println();
			decoy.shoot(rand.nextInt(10), rand.nextInt(10));
			decoy.move(rand.nextInt(10), rand.nextInt(10));
			System.out.println("<=== END TURN " + i + " ===>");
		}
	}
}

结果(控制台输出)将如下所示:

I am a scout droid
I am an ASSAULT droid
I am a DECOY droid
<=== BEGIN TURN 1 ===>
Light Laser Canon targeting: 9,0. Damage: 0.5
Moving QUICKLY to: 4,6.

Heavy laser targeting: 6,2. Damage: 2.5
Moving SLOWLY to: 9,1.

Have no weapon.
Can’t move.
<=== END TURN 1 ===>

<=== BEGIN TURN 2 ===>
Light Laser Canon targeting: 3,4. Damage: 0.5
Moving QUICKLY to: 6,5.

Reloading…
Moving SLOWLY to: 1,6.

Have no weapon.
Can’t move.
<=== END TURN 2 ===>

<=== BEGIN TURN 3 ===>
Light Laser Canon targeting: 6,7. Damage: 0.5
Moving QUICKLY to: 9,7.

Heavy laser targeting: 7,1. Damage: 2.5
Moving SLOWLY to: 2,0.

Have no weapon.
Can’t move.
<=== END TURN 3 ===>

<=== BEGIN TURN 4 ===>
Light Laser Canon targeting: 3,7. Damage: 0.5
Moving QUICKLY to: 1,4.

Reloading…
Moving SLOWLY to: 5,9.

Have no weapon.
Can’t move.
<=== END TURN 4 ===>

<=== BEGIN TURN 5 ===>
Light Laser Canon targeting: 0,8. Damage: 0.5
Moving QUICKLY to: 3,9.

Heavy laser targeting: 1,2. Damage: 2.5
Moving SLOWLY to: 3,2.

Have no weapon.
Can’t move.
<=== END TURN 5 ===>

扩展设计的挑战

机器人轮流移动并射击。 一切都很好,但是:

  • 如果要创建混合机器人,该怎么办? 能像侦察兵一样快速移动但拥有重型武器的机器人? 您将必须创建一个新类,然后从Scout和Assault机器人中复制粘贴相应的方法,对吗?
  • 还可以想象射击机制不是那么简单,它需要碰撞检测等等。 对于每个机器人,都需要重写相同的冗余代码。
  • 如果可以通过通电来增强火力怎么办?
  • 如果机器人获得了自觉性并找到了代替当前机器人使用它的武器怎么办?

我敢肯定,您对如何增强游戏玩法和扩展游戏世界有很多想法,但是最明显的解决方案(如上所述)似乎不适合这样做。 它需要创建新的droid类,并且每种droid类型都将分别实现其方法。 这些方法很多都是相同的。 当前的设计不允许您在运行时更改droid的内部结构,而无需花费大量精力。

这是一个建议的解决方案: 组合策略模式

设计Droid(正确)

一个非常简单的机器人是由放在底盘上的武器组成的。 第一个设计由“ ”类型的关系组成。 ScoutDroid 具有某些特性通用Droid

组成基于“ 具有 ”关系。 机器人 底盘机器人 拥有 武器 。 机器人具有哪种组件类型决定了它的类型。

例如,让我们分解Scout Droid。
侦察员 Droid的 具有 激光 佳能 有一轮子移动 的Droid。 我们想用轻武器使侦察员Swift行动。 另一方面, 突击 Droid 也是 Droid,但它具有 重型 激光 佳能 ,并且可以在Tracks上运行。 这使其功能非常强大,但速度有些慢。

从工厂的角度思考。 汽车生产线如何运作? 您可以在底盘上找到发动机,车轮,驱动轴,变速箱等的特定位置。
所有这些组件都是单独生产的。 产生它们的团队对其他部分一无所知。 它们必须满足一个条件:变速箱必须完全适合其位置并与发动机连接。不同的制造商具有不同的实现方式。 在这种情况下,连接器是interface 。 引擎也有类似的故事。 如果它很好地与车轮和变速箱连接,则可以安装它。 其内部设计,容量,功率,油耗可以完全不同。 发动机是汽车的组成部分之一。

我们的机器人也是如此。 但是为了简单起见,我们只有2个组件。 我们需要一个具有所有布线的通用机器人,以便其组件可以通过这些接口由机器人触发。 例如,机器人只需拉动武器的扳机,而不必关心它是什么类型的武器,只要它具有扳机即可。 机器人需要了解pullTrigger方法,并且需要实现该方法才能实现,以便我们为机器人提供武器使用。
位置的改变也是一样。 它需要触发运动。 车轮,履带或反重力推进器将把机器人带到那里。 机器人仅需要设置坐标。

为了实现这一点,我们需要一个带有实现方法而不是接口的类。
我们创建抽象的Droid类。 之所以将其抽象化,是因为我们实际上实现了触发武器,对移动机制进行动作的方法,但是我们没有将具体的武器和移动机制附加到机器人上。 具体机器人的组装将与description方法一起委托给类型构造函数。

public abstract class Droid {

	protected Weapon 	weapon;		// the weapon which will be used in fights
	protected Chassis	chassis;	// the chassis on which the droid is placed

	public void moveToPosition(int x, int y) {
		System.out.print(id + " > " );
		chassis.moveTo(x, y);
	}

	/**
	 *  Engages the position on the screen whether it is occupied by
	 *  an enemy or not. Each strategy should decide how to do it.
	 */
	public void attackPosition(int x, int y) {
		System.out.print(id + " > ");
		weapon.useWeapon(new Vector2f(x, y));
	}

	/**
	 * Displays some info on the droid
	 */
	public abstract void display();
}

如果检查该类,您将看到Droid有3种方法,从中实现2种方法。 它还具有两个组成部分武器底盘
组件是接口,因此droid在触发对它们的操作时不知道它在做什么。 所有这些都委托给实现。

接口如下:

Weapon.java

public interface Weapon {
	/**
	 * The trigger to use the weapon.
	 * @param target - where on the map is the target
	 */
	public void useWeapon(Vector2f target);

	/**
	 * Returns the description of the weapon
	 */
	public String getDescription();
}

底盘

public interface Chassis {
	/**
	 * Delegates the movement to the supporting chassis and
	 * tries to move the unit to x,y
	 */
	public void moveTo(int x, int y);

	/**
	 * Returns the description of the chassis
	 */
	public String getDescription();
}

我们将增强Droid基础类。 我们将为WeaponChassis添加setter和getter方法。 这将使我们能够在运行时更改droid的行为。 这就是战略模式的全部内容。 Droid具有行为:它可以使用武器并且可以移动 。 这两种策略(行为)需要实施。

我们还添加了一个ID ,该ID在我们的游戏中对于每个droid实例都是唯一的。 我使用一个非常简单的id生成策略。 我增加nextId静态字段,并将其附加到每种类型的构造函数中的具体droid类型前缀。

这是新的Droid类:

public abstract class Droid {

	protected static int nextId	= 0;	// the next available ID

	protected String 	id;			// unique id
	protected Weapon 	weapon;		// the weapon which will be used in fights
	protected Chassis	chassis;	// the chassis on which the droid is placed

	// the unique ID of the droid in the game
	public String getId() {
		return id;
	}

	public Weapon getWeapon() {
		return weapon;
	}
	public void setWeapon(Weapon weapon) {
		this.weapon = weapon;
	}

	public Chassis getChassis() {
		return chassis;
	}
	public void setChassis(Chassis chassis) {
		this.chassis = chassis;
	}

	public void moveToPosition(int x, int y) {
		System.out.print(id + " > " );
		chassis.moveTo(x, y);
	}

	/**
	 *  Engages the position on the screen whether it is occupied by
	 *  an enemy or not. Each strategy should decide how to do it.
	 */
	public void attackPosition(int x, int y) {
		System.out.print(id + " > ");
		weapon.useWeapon(new Vector2f(x, y));
	}

	/**
	 * Displays some info on the droid
	 */
	public abstract void display();
}

让我们制造一些武器

NoWeapon.java

/**
 * This is a null object. A null object is a dummy that does nothing and it
 * is a mere place-holder and eliminates the need to check for null.
 * @author impaler
 *
 */
public class NoWeapon implements Weapon {

	@Override
	public void useWeapon(Vector2f target) {
		// We are doing nothing
		System.out.println("No weapon equipped!");
	}

	@Override
	public String getDescription() {
		return "Nothing";
	}
}

这是空对象。 类描述应该让您知道它是什么。

LightLaserCanon.java

/**
 * This is a light laser cannon whit a quick reload time and high accuracy
 *
 * @author impaler
 *
 */
public class LightLaserCanon implements Weapon {

	private float damage = 0.5f; // the damage inflicted

	@Override
	public void useWeapon(Vector2f target) {
		System.out.println("Shooting my laser canon to " + (int)target.getX() + ","
				+ (int)target.getY() + ". Bang on! Done " + damage + " damage.");
	}

	@Override
	public String getDescription() {
		return "First generation laser canon. Street use only!";
	}
}

HeavyLaserCanon.java

/**
 * This is a heavy assault laser cannon with high accuracy but slow reload time.
 * @author impaler
 */
public class HeavyLaserCanon implements Weapon {

	private boolean loaded 	= true;	// after fire needs to be reloaded
	private float 	damage 	= 1.5f;	// the damage is considerable

	@Override
	public void useWeapon(Vector2f target) {
		if (loaded) {
			// we fire the canon
			System.out.println("Eat this! Laser beam hit target (" + (int)target.getX() + "," + (int)target.getY() + ") and dealt " + damage + " damage.");
			// next time needs reloading
			loaded = false;
		} else {
			System.out.println("Darn! Out of ammo! Reloading...");
			loaded = true;
		}
	}

	@Override
	public String getDescription() {
		return "DASS-5000 - The ultimate in siege weaponry provided by Obviam Enterprises.";
	}
}

您可能会注意到Vector2f类。 这是一个非常基本的2D向量类,当前包含x和y坐标。 而已。 您可以在下载的源中找到它。

让我们建立一些底盘

getDescription()方法说明机箱的外观

NoChassis.java –空对象(请参阅武器)

public class NoChassis implements Chassis {

	@Override
	public void moveTo(int x, int y) {
		System.out.println("It's just a frame. Can't move.");
	}

	@Override
	public String getDescription() {
		return "It's just a frame.";
	}
}

SteelStand.java

public class SteelStand implements Chassis {

	@Override
	public void moveTo(int x, int y) {
		System.out.println("Can't move.");
	}

	@Override
	public String getDescription() {
		return "Unmovable reinforced steel stand ideal for turrets and defensive units.";
	}
}

Wheels.java

public class Wheels implements Chassis {

	@Override
	public void moveTo(int x, int y) {
		System.out.println("Speeding to " + x + "," + y + " on my wheels!");
	}

	@Override
	public String getDescription() {
		return "4 wheel drive, very fast on flat terrain but struggling through obstacles.";
	}
}

Track.java

public class Track implements Chassis {

	@Override
	public void moveTo(int x, int y) {
		System.out.println("Don't get in my way! Moving slowly to: " + x + "," + y + ".");
	}

	@Override
	public String getDescription() {
		return "Slow moving tracks but able to go through many obstacles.";
	}
}

现在我们可以组装机器人了

首先让我们创建一个DecoyDroid 。 该机器人没有武器,将被放置在钢架上。 是为了我们的目标练习,还记得吗?

DecoyDroid.java

public class DecoyDroid extends Droid {

	public DecoyDroid() {
		id = "DCY-" + (++Droid.nextId);
		weapon = new NoWeapon();
		chassis = new SteelStand();
	}

	@Override
	public void display() {
		System.out.println("+--------------------------------------------------------------------------------------------+");
		System.out.println("| I am a DECOY droid.");
		System.out.println("|\tID: " + id);
		System.out.println("|\tWeapon: " + weapon.getDescription());
		System.out.println("|\tChassis: " + chassis.getDescription());
		System.out.println("+--------------------------------------------------------------------------------------------+");
	}
}

检查默认构造函数。 它创建一个id并将NoWeaponSteelStand的实例分配给该机器人。
display()方法比以前更复杂,但只是为了更好地描述droid。 它也利用了组件的描述。 如果实例化DecoyDroid并调用其显示方法,则将获得一个不错的描述。

+——————————————————————————————–+
| I am a DECOY droid.
|	ID: DCY-3
|	Weapon: Nothing
|	Chassis: Unmovable reinforced steel stand ideal for turrets and defensive units.
+——————————————————————————————–+

让我们构建其余类型:

ScoutDroid.java

public class ScoutDroid extends Droid {

	public ScoutDroid() {
		id = "SCT-" + (++Droid.nextId);
		weapon = new LightLaserCanon();
		chassis = new Wheels();
	}

	@Override
	public void display() {
		System.out.println("+--------------------------------------------------------------------------------------------+");
		System.out.println("| I am a SCOUT droid.");
		System.out.println("|\tID: " + id);
		System.out.println("|\tWeapon: " + weapon.getDescription());
		System.out.println("|\tChassis: " + chassis.getDescription());
		System.out.println("+--------------------------------------------------------------------------------------------+");
	}
}

AssaultDroid.java

public class AssaultDroid extends Droid {

	public AssaultDroid() {
		id = "ASS-" + (++Droid.nextId);
		weapon = new HeavyLaserCanon();
		chassis = new Track();
	}

	@Override
	public void display() {
		System.out.println("+--------------------------------------------------------------------------------------------+");
		System.out.println("| I am an ASSAULT droid.");
		System.out.println("|\tID: " + id);
		System.out.println("|\tWeapon: " + weapon.getDescription());
		System.out.println("|\tChassis: " + chassis.getDescription());
		System.out.println("+--------------------------------------------------------------------------------------------+");
	}
}

您会注意到,唯一需要实现的就是构造函数(它增加了底盘和武器)以及display()方法。
下图显示了新的体系结构:

让我们为其创建一个测试脚本。 我们将模拟5个回合,每个机器人将使用其武器并移动到随机位置。 检查每种武器的行为,您会发现重型激光每2圈会发射一次。 为了使它有趣,我们依次在4 中将HeavyLaserCanon赋予DecoyDroid 。 看一下它如何改变机器人的行为并开始发射。 这是在运行时动态创建的混合机器人。

模拟器代码(DroidSimulator.java):

public class DroidSimulator {

	public static void main(String[] args) {
		// for generating random numbers
		Random rand = new Random();

		Droid scout = new ScoutDroid();
		Droid assailant = new AssaultDroid();
		Droid decoy = new DecoyDroid();

		scout.display();
		assailant.display();
		decoy.display();

		// shoot-out - each droid fires once per turn
		for (int i = 1; i <= 5; i++) {
			System.out.println("\n<=== BEGIN TURN " + i + " ===>");
			// in turn 3 decoy droid is given an assault canon
			if (i == 4) {
				decoy.setWeapon(new HeavyLaserCanon());
				System.out.println("* " + decoy.getId() + " acquired " + decoy.getWeapon().getDescription() + "\n");
			}
			scout.attackPosition(rand.nextInt(10), rand.nextInt(10));	// we assume this is an enemy position
			scout.moveToPosition(rand.nextInt(10), rand.nextInt(10));
			System.out.println();
			assailant.attackPosition(rand.nextInt(10), rand.nextInt(10));
			assailant.moveToPosition(rand.nextInt(10), rand.nextInt(10));
			System.out.println();
			decoy.attackPosition(rand.nextInt(10), rand.nextInt(10));
			decoy.moveToPosition(rand.nextInt(10), rand.nextInt(10));
			System.out.println("<=== END TURN " + i + " ===>");
		}
	}
}

输出:

+——————————————————————————————–+
| I am a SCOUT droid.
|	ID: SCT-1
|	Weapon: First generation laser canon. Street use only!
|	Chassis: 4 wheel drive, very fast on flat terrain but struggling through obstacles.
+——————————————————————————————–+
+——————————————————————————————–+
| I am an ASSAULT droid.
|	ID: ASS-2
|	Weapon: DASS-5000 – The ultimate in siege weaponry provided by Obviam Enterprises.
|	Chassis: Slow moving tracks but able to go through many obstacles.
+——————————————————————————————–+
+——————————————————————————————–+
| I am a DECOY droid.
|	ID: DCY-3
|	Weapon: Nothing
|	Chassis: Unmovable reinforced steel stand ideal for turrets and defensive units.
+——————————————————————————————–+
<=== BEGIN TURN 1 ===>
SCT-1 > Shooting my laser canon to 0,3. Bang on! Done 0.5 damage.
SCT-1 > Speeding to 0,2 on my wheels!

ASS-2 > Eat this! Laser beam hit target (3,4) and dealt 1.5 damage.
ASS-2 > Don’t get in my way! Moving slowly to: 3,8.

DCY-3 > No weapon equipped!
DCY-3 > Can’t move.
<=== END TURN 1 ===>

<=== BEGIN TURN 2 ===>
SCT-1 > Shooting my laser canon to 4,0. Bang on! Done 0.5 damage.
SCT-1 > Speeding to 5,0 on my wheels!

ASS-2 > Darn! Out of ammo! Reloading…
ASS-2 > Don’t get in my way! Moving slowly to: 1,6.

DCY-3 > No weapon equipped!
DCY-3 > Can’t move.
<=== END TURN 2 ===>

<=== BEGIN TURN 3 ===>
SCT-1 > Shooting my laser canon to 3,0. Bang on! Done 0.5 damage.
SCT-1 > Speeding to 0,6 on my wheels!

ASS-2 > Eat this! Laser beam hit target (9,1) and dealt 1.5 damage.
ASS-2 > Don’t get in my way! Moving slowly to: 8,0.

DCY-3 > No weapon equipped!
DCY-3 > Can’t move.
<=== END TURN 3 ===>

<=== BEGIN TURN 4 ===>
* DCY-3 acquired DASS-5000 – The ultimate in siege weaponry provided by Obviam Enterprises.

SCT-1 > Shooting my laser canon to 8,6. Bang on! Done 0.5 damage.
SCT-1 > Speeding to 2,3 on my wheels!

ASS-2 > Darn! Out of ammo! Reloading…
ASS-2 > Don’t get in my way! Moving slowly to: 0,6.

DCY-3 > Eat this! Laser beam hit target (9,4) and dealt 1.5 damage.
DCY-3 > Can’t move.
<=== END TURN 4 ===>

<=== BEGIN TURN 5 ===>
SCT-1 > Shooting my laser canon to 1,7. Bang on! Done 0.5 damage.
SCT-1 > Speeding to 1,9 on my wheels!

ASS-2 > Eat this! Laser beam hit target (1,4) and dealt 1.5 damage.
ASS-2 > Don’t get in my way! Moving slowly to: 3,6.

DCY-3 > Darn! Out of ammo! Reloading…
DCY-3 > Can’t move.
<=== END TURN 5 ===>

依次注意4, DecoyDroid如何获得新武器并改变其行为(黄线)。 现在,您还应该了解空对象模式。

就目前而言,通过将代码保持在最低限度即可轻松创建新的武器,底盘和机器人。 在这些情况下,始终主张使用组合而不是继承。 您可以创建一个非常精致的机器人,其中包括盾牌,传感器,一系列武器以及一个AI 组件 ,该AI 组件可以根据情况决定使用哪种武器。 如果您已经注意到,我使用“使用”一词来代替火,因为一种武器也可以是近战武器,而不一定是远程武器。

可下载的项目仅包含策略模式实施。 遗漏了简单的继承方法。 尝试使用它,并掌握这个有用的模式,因为我将广泛使用它,并且它被认为是好的设计。 这是一个简单的Java应用程序,未启用Android。

此处下载(obviam.compositions.part1.tgz)

在以下各部分中,我将尝试添加一些AI功能以及如何根据其组件的图像组成最终的机器人。

参考: 设计游戏内实体。 对象组成策略。 第1部分–来自“ 反对谷物 ”博客的JCG合作伙伴Tamas Jano 的策略模式

不要忘记查看我们的新Android游戏 ArkDroid (以下屏幕截图) 。 您的反馈将大有帮助!
相关文章:

翻译自: https://www.javacodegeeks.com/2011/08/android-game-development-design-in-game.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值