游戏界面:
点击火箭开始游戏
点击Exit退出游戏
左上角显示当前成绩和历史最高分
退出自动保存最高成绩
代码获取
扫码关注微信公众号【程序猿声】
在后台回复【EBG】不包括【】即可获取。
玩法:
玩家通过鼠标控制小球所处位置
进度条缓慢减少,吃小球进度条增加
吃到黄球玩家变回初始大小
吃药进度条大幅增加,吃到炸弹游戏结束
难度等级每十份增加一级
项目包含文件:
Circle.java
GUI.java
Game.java
MyUtils.java
PaintPanel.java
PlayerCircle.java
ProgressUI.java
代码解析:
Circle.java
该文件定义了一个Circle类,用以生成每个球的对象。
属性有圆的半径、x坐标、y坐标、x方向的偏移量、y方向的偏移量、id。
方法有移动和获取每个属性的接口
package com.hust.game;
import java.awt.*;
import java.util.Random;
public class Circle {
protected int radius,x,y,dx,dy;
private int id;
protected GUI gui;
String color;
public Circle(int X,int Y,int R,int id,String color,GUI gui,int dx,int dy){
x = X;
y = Y;
radius = R;
Random random = new Random();
this.dx = random.nextBoolean() ? dx : -dx;
this.dy = random.nextBoolean() ? dy : -dy;
this.gui = gui;
this.id = id;
this.color = color;
draw();
}
public Circle(int X,int Y,int R,int id,String color,GUI gui){
x = X;
y = Y;
radius = R;
this.gui = gui;
this.id = id;
this.color = color;
}
public void draw(){
gui.updateCircle(this);
}
public void move(){
// clearcircle()
x += dx;
y += dy;
if(x + radius > gui.graphWidth || x - radius < 0){
dx = -dx;
}
if(y + radius > gui.graphHeight || y - radius < GUI.PROGRESSWIDTH){
dy = -dy;
}
draw();
}
public int getX(){
return x;
}
public int getY(){
return y;
}
public int getR(){
return radius;
}
public int getD(){
return radius << 1;
}
public int getID(){
return id;
}
}
PlayerCircle.java
该文件是用户控制的小球类。继承于Circle类,重构了移动的方法,由自动移动改为获取鼠标坐标,使用户可以通过鼠标控制小球。另外新增了Max属性用以限制用户小球的大小,避免发生小球过大以至于没有威胁的情况,增强可玩性。
package com.hust.game;
public class PlayerCircle extends Circle{
private int Max;
public PlayerCircle(int X,int Y,int R,int id,String color,GUI gui,int Maxsize){
super(X,Y,R,id,color,gui);
Max = Maxsize;
draw();
}
public void resize(int plusSize){
if(radius < Max){
radius += plusSize;
draw();
}
}
public void move(){
// clearcircle
x = gui.mouseX;
y = gui.mouseY;
draw();
}
public int getMax(){
return Max;
}
}
GUI.java
该文件是整个游戏的界面,主要功能有:游戏界面的设置、球的绘制、成绩显示等等一且UI控件的显示和设计。
package com.hust.game;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
public class GUI {
public final int graphWidth;
public final int graphHeight;
public final int STARTX = 180;
public final int STARTY = 500;
public final int buttonwidth = 100;
public final int buttonheight = 60;
public final int EXITX = 330;
public final int EXITY = 500;
public static final int PROGRESSWIDTH = 40;
public static final int BOTTOM = 70;
public int mouseX;
public int mouseY;
public Circle[] willPaint = new Circle[Game.CIRCLECOUNT+3];
public PaintPanel conn = new PaintPanel(willPaint,"paintPanel");
public JFrame jf;
public JButton start;
public JButton exit;
public JLabel currentScoreLabel;
public JLabel maxScoreLabel;
public JLabel gameLevelLabel;
public JLabel eatYourBalls;
public ProgressUI jProBar;
public GUI(){
jf = new JFrame("Big ball eat Small ball");
Toolkit kit = Toolkit.getDefaultToolkit();
graphWidth = kit.getScreenSize().width;
graphHeight = kit.getScreenSize().height-BOTTOM;
jf.setBounds(graphWidth/2-300, graphHeight/2-400, 600, 800);
jf.setLayout(null);
jf.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
conn.setLayout(null);
start = new JButton();
start.setBounds(STARTX,STARTY,64,64);
exit = new JButton();
exit.setBounds(EXITX,EXITY,64,64);
start.setIcon(new ImageIcon("./res/start.png"));
start.setMargin(new Insets(0,0,0,0));//将边框外的上下左右空间设置为0
start.setIconTextGap(0);//将标签中显示的文本和图标之间的间隔量设置为0
start.setBorderPainted(false);//不打印边框
start.setBorder(null);//除去边框
start.setFocusPainted(false);//除去焦点的框
start.setContentAreaFilled(false);//除去默认的背景填充
exit.setIcon(new ImageIcon("./res/exit.png"));
exit.setMargin(new Insets(0,0,0,0));//将边框外的上下左右空间设置为0
exit.setIconTextGap(0);//将标签中显示的文本和图标之间的间隔量设置为0
exit.setBorderPainted(false);//不打印边框
exit.setBorder(null);//除去边框
exit.setFocusPainted(false);//除去焦点的框
exit.setContentAreaFilled(false);//除去默认的背景填充
Font font1=MyUtils.getSelfDefinedFont("./res/hkww.ttc", 18);
Font font2=MyUtils.getSelfDefinedFont("./res/font.ttf", 60);
currentScoreLabel = new JLabel();
currentScoreLabel.setVisible(false);
currentScoreLabel.setFont(font1);
currentScoreLabel.setBounds(10, 40, 200, 20);
maxScoreLabel = new JLabel();
maxScoreLabel.setVisible(false);
maxScoreLabel.setFont(font1);
maxScoreLabel.setBounds(10, 60, 200, 20);
gameLevelLabel = new JLabel();
gameLevelLabel.setVisible(false);
gameLevelLabel.setFont(font1);
gameLevelLabel.setBounds(10, 80, 200, 20);
eatYourBalls = new JLabel("吃掉比你小的球!");
eatYourBalls.setVisible(true);
eatYourBalls.setFont(font2);
eatYourBalls.setBounds(60, 120, 500, 100);
eatYourBalls.setForeground(Color.decode("#8A2BE2"));
conn.add(start);
conn.add(exit);
conn.add(currentScoreLabel);
conn.add(maxScoreLabel);
conn.add(gameLevelLabel);
conn.add(eatYourBalls);
jProBar = new ProgressUI();
jProBar.getjProgressBar().setSize(graphWidth, PROGRESSWIDTH);
jProBar.getjProgressBar().setLocation(0, 0);
jProBar.getjProgressBar().setVisible(false);
conn.add(jProBar.getjProgressBar());
jf.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseDragged(MouseEvent e) {
}
@Override
public void mouseMoved(MouseEvent e) {
mouseX = e.getX();
mouseY = e.getY() - 20;
}
});
jf.setContentPane(conn);
jf.setVisible(true);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void updateCircle(Circle c){
if(c != null){
if(c.getID() == Game.CIRCLECOUNT) {
willPaint[c.getID()] = c;
jf.getContentPane().repaint();
}else {
willPaint[c.getID()] = c;
}
}
}
public void printAllEnemiesCircles() {
jf.getContentPane().repaint();
}
public void clear(){
for(int i = 0; i < willPaint.length; i++){
willPaint[i] = null;
}
jf.getContentPane().repaint();
}
}
PaintPanel.java
本文件实现具体的重绘操作。
package com.hust.game;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class PaintPanel extends JPanel{
String name;
private Circle[] willPaint;
public PaintPanel(Circle[] willPaint,String name){
this.willPaint = willPaint;
this.name = name;
}
public String toString(){
return name;
}
@Override
public void paint(Graphics g){
super.paint(g);
for(Circle nowPainting : willPaint){
int maxBoomsNr = 0;
int maxLifesNr = 0;
if(nowPainting != null){
// System.out.println(nowPainting.color + " " + nowPainting.getID());
if(nowPainting.color.equals("#EED5D2") && (maxBoomsNr <= 3)) {
Image image=Toolkit.getDefaultToolkit().getImage("./res/boom.png");
nowPainting.radius = 16;
g.drawImage(image, nowPainting.getX(), nowPainting.getY(), this);
maxBoomsNr++;
continue;
}else if(nowPainting.color.equals("#6A5ACD") && (maxLifesNr <= 3)) {
Image image=Toolkit.getDefaultToolkit().getImage("./res/life.png");
nowPainting.radius = 16;
g.drawImage(image, nowPainting.getX(), nowPainting.getY(), this);
maxLifesNr++;
continue;
}
g.setColor(Color.decode(nowPainting.color));
g.fillOval(nowPainting.getX() - nowPainting.getR(),nowPainting.getY() - nowPainting.getR(),
nowPainting.getD(),nowPainting.getD());
}
}
}
}
MyUtils.java
本文件实现一些工具函数的整合,比如获取随机颜色、读取成绩、将成绩写入文件等。
package com.hust.game;
import java.awt.Color;
import java.awt.FontFormatException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Random;
import java.util.Scanner;
public class MyUtils {
static String getRandomColor(Random r) {
int count=(int) (r.nextInt(18)+1);
String returnColor;
switch (count) {
case 1:
returnColor = "#00FF00";
break;
case 2:
returnColor = "#00FFFF";
break;
case 3:
returnColor = "#00BFFF";
break;
case 4:
returnColor = "#8A7CDA";
break;
case 5: //yellow , reset player small
returnColor = "#FF00FF";
break;
case 6:
returnColor = "#FF0000";
break;
case 7:
returnColor = "#FA663C";
break;
case 8:
returnColor = "#B5B5B5";
break;
case 9:
returnColor = "#BF487A";
break;
case 10:
returnColor = "#90EE90";
break;
case 11:
returnColor = "#C978E7";
break;
case 12: //boom
returnColor = "#EED5D2";
break;
case 13:
returnColor = "#52EA41";
break;
case 14:
returnColor = "#FF7F00";
break;
case 15:
returnColor = "#8B658B";
break;
case 16:
returnColor = "#7FFFD4";
break;
case 17: //life
returnColor = "#6A5ACD";
break;
case 18:
returnColor = "#9BCD9B";
break;
default:
returnColor = "#8B0A50";
break;
}
return returnColor;
}
//filepath字体文件的路径
public static java.awt.Font getSelfDefinedFont(String filepath, int size){
java.awt.Font font = null;
File file = new File(filepath);
try{
font = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, file);
font = font.deriveFont(java.awt.Font.PLAIN, size);
}catch (FontFormatException e){
return null;
}catch (FileNotFoundException e){
return null;
}catch (IOException e){
return null;
}
return font;
}
public static int readRecordFromFile(String filename) {
int record = 0;
try {
Scanner in = new Scanner(new FileReader(filename));
if(in.hasNext()) {
record = in.nextInt();
}else {
System.out.println("no record!");
}
in.close();
} catch (FileNotFoundException e) {
// File not found
System.out.println("File not found!");
}
return record;
}
public static void writeRecordInFile(String filename, int record) {
File file = new File(filename);
Writer writer = null;
StringBuilder outputString = new StringBuilder();
try {
outputString.append(record);
writer = new FileWriter(file, false); // true表示追加
writer.write(outputString.toString());
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Game.java
本文件实现具体的操作逻辑,分别对于用户操作的小球和游戏控制的小球建立两个新的线程,以及建立一个检测碰撞和生死的线程。还有更新成绩、更新难度等级、游戏结束回收资源等操作。并且还要更新游戏的进度条。
package com.hust.game;
import javax.swing.*;
import java.awt.Color;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.PointerInfo;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Random;
import java.util.Scanner;
public class Game {
public boolean boom(Circle a, Circle b) {
return a.getR() + b.getR() >=
Math.sqrt(Math.pow(a.getX() - b.getX(), 2) + Math.pow(a.getY() - b.getY(), 2));
}
public void backMain(GUI gui){
gui.jf.getContentPane().setBackground(initColor);
gui.jf.setBounds(gui.graphWidth/2-300, gui.graphHeight/2-400, 600, 800);
gui.exit.setVisible(true);
gui.start.setVisible(true);
gui.eatYourBalls.setText("再试试看吧!");
gui.eatYourBalls.setVisible(true);
gui.eatYourBalls.setBounds(120, 120, 500, 100);
gui.jProBar.getjProgressBar().setVisible(false);
gui.maxScoreLabel.setText("历史最高:"+historyScore);
gui.clear();
new Circle(240, 350, 80, Game.CIRCLECOUNT+1, "#00CED1",gui, 0, 0);
new Circle(350, 380, 40, Game.CIRCLECOUNT+2, "#ADFF2F",gui, 0, 0);
gui.jf.getContentPane().repaint();
}
public static final int ORIGNALR = 15;
public static final int CIRCLECOUNT = 50;
public static final int MAX = 100;
public static final int MIN = 10;
public static int historyScore = 0;
public static volatile int score = 0;
public static volatile boolean gameplaying;
public static volatile boolean lookingscore;
public static Color initColor;
public static int enemyMovingSpeed = 100;
public static Random random;
class PLAY {
public int cnt;
public int score;
public PLAY(int cnt, int score) {
this.cnt = cnt;
this.score = score;
}
}
public synchronized void startGame(final GUI gui) throws InterruptedException {
final PlayerCircle[] player = {new PlayerCircle(gui.mouseX, gui.mouseY, ORIGNALR, CIRCLECOUNT, "#000000", gui, MAX)};
final Circle[] enemies = new Circle[CIRCLECOUNT];
score = 0;
gameplaying = true;
random = new Random();
for (int i = 0; i < CIRCLECOUNT; i++) {
if (i < CIRCLECOUNT / 4 * 3) {
int enermyR = random.nextInt(player[0].getR()) + MIN;
if (i == CIRCLECOUNT / 2) {
do {
enemies[i] = new Circle(random.nextInt(gui.graphWidth - enermyR * 2) + enermyR, random.nextInt(gui.graphHeight
- enermyR * 2-GUI.BOTTOM) + enermyR+GUI.PROGRESSWIDTH, enermyR, i, "#FFFF00",
gui, random.nextInt(10) + 1, random.nextInt(10) + 1);
} while (boom(enemies[i], player[0]));
}else {
do {
enemies[i] = new Circle(random.nextInt(gui.graphWidth - enermyR * 2) + enermyR, random.nextInt(gui.graphHeight
- enermyR * 2-GUI.BOTTOM) + enermyR+GUI.PROGRESSWIDTH, enermyR, i, MyUtils.getRandomColor(random),
gui, random.nextInt(10) + 1, random.nextInt(10) + 1);
} while (boom(enemies[i], player[0]));
}
} else {
int enermyR = random.nextInt(MAX - player[0].getR()) + player[0].getR();
do {
enemies[i] = new Circle(random.nextInt(gui.graphWidth - enermyR * 2) + enermyR, random.nextInt(gui.graphHeight
- enermyR * 2-GUI.BOTTOM) + enermyR+GUI.PROGRESSWIDTH, enermyR, i, MyUtils.getRandomColor(random),
gui, random.nextInt(3) + 1, random.nextInt(3) + 1);
} while (boom(enemies[i], player[0]));
}
}
gui.jf.getContentPane().repaint();
System.out.println("start and score has already unvisible");
class playerMovingCircle implements Runnable {
@Override
public synchronized void run() {
System.out.println("player moving");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (gameplaying && player[0] != null) {
player[0].move();
}
System.out.println("player done");
}
}
class enemyMoving implements Runnable {
public synchronized void run() {
System.out.println("enemies moving");
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
while (gameplaying && player[0] != null) {
for (int i = 0; i < enemies.length; i++) {
if (enemies[i] == null) {
int enermyR;
if (i < CIRCLECOUNT / 4 * 3) {
enermyR = random.nextInt(player[0].getR() - MIN) + MIN;
} else {
enermyR = random.nextInt(MAX - player[0].getR()) + player[0].getR();
}
do{
enemies[i] = new Circle(random.nextInt(gui.graphWidth - enermyR * 2) + enermyR, random.nextInt(gui.graphHeight
- enermyR * 2-GUI.BOTTOM) + enermyR+GUI.PROGRESSWIDTH, enermyR, i, MyUtils.getRandomColor(random),
gui, random.nextInt(3) + 1, random.nextInt(3) + 1);
} while (boom(enemies[i], player[0]));
if (i == CIRCLECOUNT / 2) {
do {
enemies[i] = new Circle(random.nextInt(gui.graphWidth - enermyR * 2) + enermyR, random.nextInt(gui.graphHeight
- enermyR * 2-GUI.BOTTOM) + enermyR+GUI.PROGRESSWIDTH, enermyR, i, "#FFFF00",
gui, random.nextInt(10) + 1, random.nextInt(10) + 1);
} while (boom(enemies[i], player[0]));
}
}
enemies[i].move();
}
gui.printAllEnemiesCircles(); //鏇存柊鎵�鏈�
try {
Thread.sleep(enemyMovingSpeed);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("enemies done");
}
}
class countScore implements Runnable {
public synchronized void run() {
System.out.println("counting score");
while (gameplaying) {
for (int i = 0; i < enemies.length; i++) {
if (enemies[i] != null && player[0] != null) {
if (boom(enemies[i], player[0])) {
//炸弹检测
if(enemies[i].color.equals("#EED5D2")) {
gameplaying = false;
break;
}
//药 检测
if(enemies[i].color.equals("#6A5ACD")) {
gui.jProBar.addValue(5);
enemies[i] = null;
continue;
}
//普通球检测
if (player[0].getR() > enemies[i].getR()) {
if (i != CIRCLECOUNT / 2) {
player[0].resize(1);
score++;
gui.currentScoreLabel.setText("当前成绩:" + score);
if(score != 0 && score % 10 == 0) {
if(enemyMovingSpeed > 40) {
enemyMovingSpeed -= 20;
}else if(enemyMovingSpeed > 20) {
enemyMovingSpeed -= 5;
}else if(enemyMovingSpeed > 1) {
enemyMovingSpeed -= 1;
}
gui.gameLevelLabel.setText("难度等级:" + (100- enemyMovingSpeed));
}
gui.jProBar.addValue(3);
} else {
player[0].resize(-1 * (player[0].getR() - ORIGNALR));
}
enemies[i] = null;
} else {
gameplaying = false;
break;
}
}
}
}
}
//娓告垙缁撴潫锛屾敹灏�
for (int i = 0; i < enemies.length; i++) {
enemies[i] = null;
}
if(score > historyScore) {
historyScore = score;
}
player[0] = null;
gui.jf.getContentPane().setBackground(Color.RED);
//gui.jf.getContentPane().repaint();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
backMain(gui);
System.out.println(score);
}
}
class progressUI implements Runnable {
public synchronized void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while(gui.jProBar.getValue() > 0 && gameplaying) {
gui.jProBar.addValue(-1);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
gameplaying = false;
}
}
playerMovingCircle pmc = new playerMovingCircle();
Thread playerMC = new Thread(pmc);
enemyMoving em = new enemyMoving();
Thread eM = new Thread(em);
countScore cs = new countScore();
Thread cS = new Thread(cs);
progressUI pUI = new progressUI();
Thread tProgress = new Thread(pUI);
System.out.println("杩涚▼瀹氫箟瀹屾瘯");
playerMC.start();
eM.start();
cS.start();
tProgress.start();
System.out.println("涓荤嚎绋媟unning");
// gui.jf.setContentPane(oriView);
}
public static void main(String[] args) {
final Game mian = new Game();
final GUI gui = new GUI();
System.out.println("gui鍒涘缓瀹屾瘯");
gameplaying = false;
initColor = gui.jf.getContentPane().getBackground();
historyScore = MyUtils.readRecordFromFile("./res/record.txt");
new Circle(240, 350, 80, Game.CIRCLECOUNT+1, "#00CED1",gui, 0, 0);
new Circle(350, 380, 40, Game.CIRCLECOUNT+2, "#ADFF2F",gui, 0, 0);
gui.jf.getContentPane().repaint();
gui.start.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
gui.start.setVisible(false);
gui.exit.setVisible(false);
gui.eatYourBalls.setVisible(false);
gui.currentScoreLabel.setVisible(true);
gui.maxScoreLabel.setVisible(true);
gui.gameLevelLabel.setVisible(true);
score = 0;
enemyMovingSpeed = 100;
gui.currentScoreLabel.setText("当前成绩:" + score);
gui.maxScoreLabel.setText( "历史最高:" + historyScore);
gui.gameLevelLabel.setText( "难度等级:" + (100-enemyMovingSpeed));
gui.clear();
gui.jProBar.getjProgressBar().setValue(100);
gui.jProBar.getjProgressBar().setVisible(true);
gui.jf.setExtendedState(JFrame.MAXIMIZED_BOTH);
try {
mian.startGame(gui);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
});
gui.jf.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
MyUtils.writeRecordInFile("./res/record.txt", historyScore);
}
});
gui.exit.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
MyUtils.writeRecordInFile("./res/record.txt", historyScore);
System.exit(0);
}
});
}
}
ProgressUI.java
该文件控制进度条的增减。
package com.hust.game;
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JComponent;
import javax.swing.JProgressBar;
import javax.swing.plaf.basic.BasicProgressBarUI;
public class ProgressUI {
private JProgressBar jProgressBar;
private int progressvalue;
ProgressUI() {
this.jProgressBar = new JProgressBar();
}
ProgressUI(JProgressBar jProgressBar) {
this.jProgressBar = jProgressBar;
}
public void addValue(int num) {
progressvalue=this.jProgressBar.getValue() + num;
this.jProgressBar.setValue(progressvalue);
if(progressvalue<20){
this.jProgressBar.setForeground(Color.RED);
}
else if(progressvalue<40){
this.jProgressBar.setForeground(Color.YELLOW);
}
else if(progressvalue<60){
this.jProgressBar.setForeground(Color.BLUE);
}
else if(progressvalue<80){
this.jProgressBar.setForeground(Color.GREEN);
}
else{
this.jProgressBar.setForeground(Color.CYAN);
}
this.jProgressBar.setValue(progressvalue);
}
public int getValue() {
return jProgressBar.getValue();
}
public JProgressBar getjProgressBar() {
return jProgressBar;
}
public void setjProgressBar(JProgressBar jProgressBar) {
this.jProgressBar = jProgressBar;
}
}
小结:
采用git版本控制,Github云平台协同开发。
基于传统大球吃小球游戏,融入更多新鲜有趣元素。
游戏逻辑、代码框架等由小组各成员自主思考完成
扩展:
- 键盘控制模式
- 敌人的球相互碰撞反弹
- 网络双人对战模式、多人挑战模式