开发Windows贪吃蛇游戏——(二)代码实现

前言

在上一篇博客中 开发Windows贪吃蛇游戏——(一)前期准备 我们对贪吃蛇游戏有了一定的分析,这次就对其中的功能做一下具体的 代码实现

窗体部分

首先创建主类,命名为Game.java,代码如下。

public class Game {
    public static void main(String[] args){
        new Frame();
    }
}

此处只需简单调用Frame的构造方法,创建一个新的Frame框架即可。

Frame部分

建立Frame类,命名为Frame.java。
在Frame类中 暂时 只需要简单调用生成一个新的Panel面板,后续在实现了基本功能后再对Frame进行一些修改。

//generate game main panel
this.add(new Panel());
this.setTitle("Snake Game");
//define the close operation
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setResizable(false);
//adaptive size
this.pack();
this.setVisible(true);
//set location to center
this.setLocationRelativeTo(null);

接下来我们使用Panel生成主体,在Panel.java中生成以下方法。

public class Panel extends JPanel implements ActionListener{
	public void startGame();
	public void paintComponent(Graphics g);
	public void drawGame(Graphics g);
	public void newSnake();
	public void newFood();
	public void moveSnake();
	public void checkFoods();
	public void checkCollisions();
	public void gameOver(Graphics g);
	public void gamePause(Graphics g);
	public void gameWelcome(Graphics g);
	@Override
	public void actionPerformed(ActionEvent e);
}
public class GameKeyAdapter extends KeyAdapter{
    @Override
    public void keyPressed(KeyEvent e);
}

声明全局变量

本项目中有很多全局变量,有些是boolean类型,作为Flag标志位;有些则是final的,单纯作为常量使用,后面会详细的一一介绍。

//basic configuration, mostly used parameters are down below
//800*600 1020*760 1360*760 1400*1040
//1600*900 1920*1080 2560*1440 4080*2160
static int SCREEN_WIDTH = 800;
static int SCREEN_HEIGHT = 600;
static final int UNIT_SIZE = 20;
static final int GAME_UNITS =
        SCREEN_WIDTH * SCREEN_HEIGHT / UNIT_SIZE;
//speed | update gap
static int DELAY = 200;
//snake storage
final int[] x =new int[GAME_UNITS];
final int[] y =new int[GAME_UNITS];
//original snake length
int bodyParts = 4;
//spawn detection
int tempSnake = 1;
//score & part of the body length
int foodEaten = 0;
//food position
int foodX = 0;
int foodY = 0;
//direction
char runningDirection = 'R';
//serve for single input judgement
char reservedDirection = 'R';
//determine the whole game state
boolean runningState = false;
//determine pause state
boolean pauseTrigger = false;
//determine whether the game is at the initial window
boolean welcomeTrigger = true;
//serve for difficulty change by time
int tempDifficultyCounts = 0;
//colorful snake temp
boolean redColorDecrease = true;
boolean greenColorDecrease = true;
boolean blueColorDecrease = true;
int redColor = 102;
int greenColor = 204;
int blueColor = 255;
//game clock
Timer timer;
//random generate for food position
Random random;

Panel的初始化

在Panel的构造方法中,初始化random随机对象,调用startGame方法,启动整个游戏。

Panel(){
    random = new Random();
    //set resolution
    this.setPreferredSize(
            new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
    //set background color
    this.setBackground(Color.black);
    this.setFocusable(true);
    this.addKeyListener(new GameKeyAdapter());
    startGame();
}

startGame方法

调用newSnake方法,创建一个新的蛇,调用newFood方法,创建一个新的食物。接着设置游戏状态标志位runningState为true,此标志位一旦为false,即代表游戏结束。最后初始化timer,但是不启动timer,等用户跳过欢迎界面时再启动timer,开始定时刷新。

//new game initialization with default snake & food
newSnake();
newFood();
//set running state
runningState = true;
timer = new Timer(DELAY, this);
//start clock and stop when game over

paintComponent方法

要注意,paintComponent是Java Swing的一个方法,类似于Main方法,是自动执行的,执行后调用drawGame,用于绘制当前帧。

//set graphics
super.paintComponent(g);
drawGame(g);

drawGame方法

在drawGame中需要绘制地图、墙、食物、蛇、欢迎界面、暂停界面、结束界面,具体实现如下。
遍历整个地图,利用drawLine方法划线,生成网格状地图。

//draw grid
for (int i = 0; i < SCREEN_WIDTH / UNIT_SIZE; i++) {
    g.drawLine(i * UNIT_SIZE, 0,
            i * UNIT_SIZE, SCREEN_HEIGHT);
}
for (int i = 0; i < SCREEN_HEIGHT / UNIT_SIZE; i++) {
    g.drawLine(0, i * UNIT_SIZE,
            SCREEN_WIDTH, i * UNIT_SIZE);
}

将四周最外围的格子作为墙,利用fillRect方法填充成矩形,颜色要和背景颜色区分开。

//draw walls
g.setColor(new Color(212, 242, 231));
for (int i = 0; i < SCREEN_WIDTH / UNIT_SIZE; i++) {
    g.fillRect(i * UNIT_SIZE, 0,
            UNIT_SIZE, UNIT_SIZE);
    g.fillRect(i * UNIT_SIZE, SCREEN_HEIGHT - UNIT_SIZE,
            UNIT_SIZE, UNIT_SIZE);
}
for (int i = 0; i < SCREEN_HEIGHT / UNIT_SIZE; i++) {
    g.fillRect(0, i * UNIT_SIZE,
            UNIT_SIZE, UNIT_SIZE);
    g.fillRect(SCREEN_WIDTH - UNIT_SIZE, i * UNIT_SIZE,
            UNIT_SIZE, UNIT_SIZE);
}

当用户未跳过欢迎界面时,绘制欢迎界面,调用gameWelcome方法。

if(welcomeTrigger){
    gameWelcome(g);
}

当用户已跳过欢迎界面时,绘制剩余内容,首先绘制食物和蛇。此处的蛇头和蛇身颜色有所区分,绘制前初始化全局变量redColo、greenColor、blueColor为指定的RGB颜色,蛇身本身由两个if循环和一个标志位限制,产生渐变色。其中关键是控制RGB的值在0~255之间,且能够每次有小幅变动。

else{
    //draw food
    g.setColor(new Color(0, 255, 127));
    g.fillRoundRect(foodX + 2, foodY + 2,
            UNIT_SIZE - 4, UNIT_SIZE - 4,
            UNIT_SIZE - 4, UNIT_SIZE - 4);
    //draw snake with unique head color
    redColor = 102;
    greenColor = 204;
    blueColor = 255;
    redColorDecrease = true;
    greenColorDecrease = true;
    blueColorDecrease = true;
    for(int i = 0; i < bodyParts + foodEaten; i++){
        if(i == 0){
            g.setColor(Color.blue);
        }
        else{
            if(redColorDecrease){
                redColor -= 3;
                if(redColor < 10){
                    redColorDecrease = false;
                }
            }
            else{
                redColor += 3;
                if(redColor > 245){
                    redColorDecrease = true;
                }
            }
            if(greenColorDecrease){
                greenColor -= 4;
                if(greenColor < 10){
                    greenColorDecrease = false;
                }
            }
            else{
                greenColor += 4;
                if(greenColor > 245){
                    greenColorDecrease = true;
                }
            }
            if(blueColorDecrease){
                blueColor -= 5;
                if(blueColor < 10){
                    blueColorDecrease = false;
                }
            }
            else{
                blueColor += 5;
                if(blueColor > 245){
                    blueColorDecrease = true;
                }
            }
            g.setColor(new Color(redColor, greenColor, blueColor));
        }
        g.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE);
    }
}

当蛇死亡时,将蛇头设置为红色,同时调用gameOver方法,绘制结束界面。

//draw dead snake head
if(!runningState){
    g.setColor(Color.red);
    g.fillRect(x[0], y[0], UNIT_SIZE, UNIT_SIZE);
    gameOver(g);
}

当在运行中触发游戏暂停时,调用gamePause方法,停止下一帧的运算,并绘制暂停界面。

//pause without stop clock
if(pauseTrigger){
    gamePause(g);
}

newSnake方法

在屏幕中央附近绘制蛇,方向为横向,且蛇头朝右,若蛇长度超过了横向的单元格个数,将报错。

if(tempSnake > SCREEN_WIDTH / UNIT_SIZE){
    JOptionPane.showMessageDialog(this,
            "Spawn snake error!", "Error",
            JOptionPane.ERROR_MESSAGE);
}
for(int i = bodyParts; i > 0; i--){
    //set default position
    x[i - 1] = tempSnake * UNIT_SIZE;
    y[i - 1] = (SCREEN_HEIGHT / UNIT_SIZE / 2)
            * UNIT_SIZE;
    tempSnake ++;
}
//reset to default
tempSnake = 1;

newFood方法

绘制食物可以利用之前的random产生随机数,但是要注意,生成后需要检测是否和墙重叠,或者和蛇头重叠,一旦重叠需要重新生成。

//set random position in map except the walls
foodX = random.nextInt((SCREEN_WIDTH / UNIT_SIZE) - 2)
        * UNIT_SIZE + UNIT_SIZE;
foodY = random.nextInt((SCREEN_HEIGHT / UNIT_SIZE) - 2)
        * UNIT_SIZE + UNIT_SIZE;
//detect pre-collision with snake head & respawn
if((x[0] == foodX) && (y[0] == foodY)){
    newFood();
}

moveSnake方法

在重绘每一帧之前,都需要重新计算蛇的移动,利用runningDirection标志位,判断蛇的移动,然后利用for循环,将蛇身前一节的位置赋给后一节,若此时蛇吃到了食物,会被后续的函数判断,并增长蛇身,在本方法中无需额外考虑。

//copy the former one from the tail to the neck
for(int i = bodyParts + foodEaten; i > 0; i--){
    x[i] = x[i - 1];
    y[i] = y[i - 1];
}
//set the head position
switch(runningDirection){
    case 'U':
        y[0] = y[0] - UNIT_SIZE;
    break;
    case 'D':
        y[0] = y[0] + UNIT_SIZE;
        break;
    case 'L':
        x[0] = x[0] - UNIT_SIZE;
        break;
    case 'R':
        x[0] = x[0] + UNIT_SIZE;
        break;
}

checkFood方法

检测是否蛇头和食物重叠,若重叠说明吃到食物,需要重新生成食物,并增加蛇身长度,此处也减少了每一帧绘制的延迟,增加了游戏难度。

if((x[0] == foodX) && (y[0] == foodY)){
    //score & snake length increase
    foodEaten++;
    //increase difficulty
    if(DELAY > 50){
        DELAY--;
    }
    //respawn food
    newFood();
}

checkCollisions方法

检测蛇是否与墙或蛇身碰撞,一旦碰撞后将runningState 标志位置为false,同时停止计时器,准备进入结束界面。

//check snake body with snake head
for(int i = 1; i < bodyParts + foodEaten; i++){
    if ((x[i] == x[0]) && (y[i] == y[0])) {
        runningState = false;
        break;
    }
}
//check the whole snake with the walls
if((x[0] == 0) || (x[0] == SCREEN_WIDTH - UNIT_SIZE)
|| (y[0] == 0) || (y[0] == SCREEN_HEIGHT - UNIT_SIZE)){
    runningState = false;
}
if(!runningState){
    timer.stop();
}

gameOver方法

被调用后再屏幕中央绘制Game Over字样,在下方绘制分数。

//ending marks
g.setColor(new Color(139,0,0));
g.setFont(new Font("Consolas", Font.BOLD, 70));
FontMetrics metricsUp = getFontMetrics(g.getFont());
g.drawString("Game Over",
        (SCREEN_WIDTH -
                metricsUp.stringWidth("Game Over")) / 2,
        SCREEN_HEIGHT / 2 - 50);
//final scores
g.setColor(new Color(255,250,250));
g.setFont(new Font("Consolas", Font.BOLD, 30));
FontMetrics metricsDown = getFontMetrics(g.getFont());
g.drawString("Your Score: " + foodEaten,
        (SCREEN_WIDTH -
                metricsDown.stringWidth(
                        "Your Score: " + foodEaten)) / 2,
        SCREEN_HEIGHT / 2 + 50);

gamePause和gameWelcome方法

gamePause和gameOver类似,被调用后再屏幕中央绘制Game Pause字样,在下方绘制分数,具体代码参考上文。
gameWelcome则不显示分数,显示Press space to start。

actionPerformed方法

actionPerformed是游戏能运行的关键,当游戏处于运行状态时,先锁存runningDirection到reservedDirection,再调用moveSnake方法移动蛇的位置;调用checkFoods方法检测是否吃到食物;调用checkCollisions方法检测是否撞到墙或自身;同时引入tempDifficultyCounts标志位,每五次刷新就增加一些难度,以上都执行完毕后调用repaint方法重绘新的窗口。此处锁存是由于键盘可以在两帧之间输入多次,需要过滤冗余的输入。

//frame change
if((!pauseTrigger) && runningState) {
    reservedDirection = runningDirection;
    moveSnake();
    checkFoods();
    checkCollisions();
    //increase difficulty by time
    if (tempDifficultyCounts == 5) {
        tempDifficultyCounts = 0;
        if(DELAY > 50){
            DELAY--;
        }
    }
    else{
        tempDifficultyCounts++;
    }
}
repaint();

GameKeyAdapter类

这个类比较特殊,主要是用来接收键盘输入的,需要extends继承KeyAdapter类,重载keyPressed方法,获得KeyEvent对象,然后加以判别。此处涉及到键盘的上下左右键,用来修改runningDirection标志位,其中,当锁存的reservedDirection标志位和键盘输入的方向相反时,将此次输入视为无效,此处仅给出一个方向的例子,其他方向类似。

switch(e.getKeyCode()){
	//4 directions
	case KeyEvent.VK_LEFT:
	    if(reservedDirection != 'R'){
	        runningDirection = 'L';
	    }
	    if(reservedDirection == 'L'){
	        if(!welcomeTrigger){
	            if((pauseTrigger) && (runningState)) {
	                pauseTrigger = false;
	            }
	        }
	    }
	    break;
}

当按下键盘的P键,若此时游戏已经运行,需要将pauseTrigger取反,即P键可以暂停,也可以继续游戏。

case KeyEvent.VK_P:
	//pause
	if(!welcomeTrigger){
	    pauseTrigger = (!pauseTrigger) && (runningState);
	}
	break;

类似的还有空格键,按下后将welcomeTrigger置为false,R键,按下后需要先将timer停止,接着将welcomeTrigger置为false,然后将前面除了welcomeTrigger以外的全局变量重新初始化,最后重新调用startGame方法,并启动timer。

Frame的改进

书接上文,至此,绝大部分功能都已实现,但是为了增加实用性,我们需要在Frame上添加菜单,添加背景音乐,修改程序图标,最终实现修改分辨率。

菜单及子菜单的生成

菜单的设计共有两栏,分别为Game和Settings,其中Game包含一级子菜单Start、Pause & Resume、Quit;Settings包含一级子菜单Resolution和Music,Resolution包含二级子菜单800 x 600、1020 x 760、1360 x 760、1400 x 1040、1600 x 900、1920 x 1080、2560 x 1440、3840 x 2160;Music包含二级子菜单On和Off。
其中的关键是需要注意MenuItem的类型,是按钮、单选还是复选,若是后两者则需绑定MenuItem到同一Group内,单选框可以提前设置默认选择。

JMenuBar menuBar = new JMenuBar();
//menu game control
JMenu gameMenu = new JMenu("Game");
//sub menu buttons
JMenuItem startMenuItem = new JMenuItem("Start");
JMenuItem pauseMenuItem = new JMenuItem("Pause & Resume");
JMenuItem quitMenuItem = new JMenuItem("Quit");
//binding sub button to main button
gameMenu.add(startMenuItem);
gameMenu.add(pauseMenuItem);
gameMenu.add(quitMenuItem);
//menu game settings
JMenu settingMenu = new JMenu("Settings");
//sub menu resolution
JMenu resolutionMenu = new JMenu("Resolution");
//resolution choices with only single choice
JRadioButtonMenuItem resolutionMenuItem1 =
        new JRadioButtonMenuItem("800 x 600");
JRadioButtonMenuItem resolutionMenuItem2 =
        new JRadioButtonMenuItem("1020 x 760");
JRadioButtonMenuItem resolutionMenuItem3 =
        new JRadioButtonMenuItem("1360 x 760");
JRadioButtonMenuItem resolutionMenuItem4 =
        new JRadioButtonMenuItem("1400 x 1040");
JRadioButtonMenuItem resolutionMenuItem5 =
        new JRadioButtonMenuItem("1600 x 900");
JRadioButtonMenuItem resolutionMenuItem6 =
        new JRadioButtonMenuItem("1920 x 1080");
JRadioButtonMenuItem resolutionMenuItem7 =
        new JRadioButtonMenuItem("2560 x 1440");
JRadioButtonMenuItem resolutionMenuItem8 =
        new JRadioButtonMenuItem("3840 x 2160");
//binding into one group to activate single choice
ButtonGroup resolutionGroup = new ButtonGroup();
resolutionGroup.add(resolutionMenuItem1);
resolutionGroup.add(resolutionMenuItem2);
resolutionGroup.add(resolutionMenuItem3);
resolutionGroup.add(resolutionMenuItem4);
resolutionGroup.add(resolutionMenuItem5);
resolutionGroup.add(resolutionMenuItem6);
resolutionGroup.add(resolutionMenuItem7);
resolutionGroup.add(resolutionMenuItem8);
//default choice
resolutionMenuItem1.setSelected(true);
//binding choices to sub button
resolutionMenu.add(resolutionMenuItem1);
resolutionMenu.add(resolutionMenuItem2);
resolutionMenu.add(resolutionMenuItem3);
resolutionMenu.add(resolutionMenuItem4);
resolutionMenu.add(resolutionMenuItem5);
resolutionMenu.add(resolutionMenuItem6);
resolutionMenu.add(resolutionMenuItem7);
resolutionMenu.add(resolutionMenuItem8);
//binding sub button to main button
settingMenu.add(resolutionMenu);

JMenu bgmMenu = new JMenu("Music");

JRadioButtonMenuItem bgmMenuItem1 =
        new JRadioButtonMenuItem("On");
JRadioButtonMenuItem bgmMenuItem2 =
        new JRadioButtonMenuItem("Off");

ButtonGroup bgmGroup = new ButtonGroup();
bgmGroup.add(bgmMenuItem1);
bgmGroup.add(bgmMenuItem2);

bgmMenuItem1.setSelected(true);
bgmMenu.add(bgmMenuItem1);
bgmMenu.add(bgmMenuItem2);

settingMenu.add(bgmMenu);

//binding main button to main bar
menuBar.add(gameMenu);
//binding main button to main bar
menuBar.add(settingMenu);

设计好菜单内容后,需要将菜单的触发绑定到对应的监听上。若触发Start,则模拟键盘输入R,利用预先写好的Panel内的方法帮助我们实现启动游戏的功能;同理,触发Pause & Resume后模拟输入P;触发Quit则直接调用System.exit(0)结束进程。触发对应的分辨率,则强制设置Frame大小为对应的分辨率,修改Panel内的全局变量SCREEN_WIDTH、SCREEN_HEIGHT为对应大小,最后调用后文的restart方法重启游戏;当触发On和Off时,调用start和stop方法控制背景音乐,这里下文会详细讲解。

//hit start to trigger Key_R and restart the panel
startMenuItem.addActionListener(e -> {
    try {
        Robot robot = new Robot();
        robot.keyPress(KeyEvent.VK_R);
        robot.keyRelease(KeyEvent.VK_R);
    }
    catch (AWTException eA) {
        eA.printStackTrace();
    }
});
//hit pause to trigger Key_P and pause the panel
pauseMenuItem.addActionListener(e -> {
    try {
        Robot robot = new Robot();
        robot.keyPress(KeyEvent.VK_P);
        robot.keyRelease(KeyEvent.VK_P);
    }
    catch (AWTException eA) {
        eA.printStackTrace();
    }
});
//hit quit to exit
quitMenuItem.addActionListener(e -> System.exit(0));
//hit resolution to trigger restart method
resolutionMenuItem1.addActionListener(e -> {
    Panel.SCREEN_WIDTH = 800;
    Panel.SCREEN_HEIGHT = 540;
    restart(Panel.SCREEN_WIDTH, Panel.SCREEN_HEIGHT);
});
resolutionMenuItem2.addActionListener(e -> {
    Panel.SCREEN_WIDTH = 1020;
    Panel.SCREEN_HEIGHT = 700;
    restart(Panel.SCREEN_WIDTH, Panel.SCREEN_HEIGHT);
});
resolutionMenuItem3.addActionListener(e -> {
    Panel.SCREEN_WIDTH = 1360;
    Panel.SCREEN_HEIGHT = 700;
    restart(Panel.SCREEN_WIDTH, Panel.SCREEN_HEIGHT);
});
resolutionMenuItem4.addActionListener(e -> {
    Panel.SCREEN_WIDTH = 1400;
    Panel.SCREEN_HEIGHT = 980;
    restart(Panel.SCREEN_WIDTH, Panel.SCREEN_HEIGHT);
});
resolutionMenuItem5.addActionListener(e -> {
    Panel.SCREEN_WIDTH = 1600;
    Panel.SCREEN_HEIGHT = 840;
    restart(Panel.SCREEN_WIDTH, Panel.SCREEN_HEIGHT);
});
resolutionMenuItem6.addActionListener(e -> {
    Panel.SCREEN_WIDTH = 1920;
    Panel.SCREEN_HEIGHT = 1020;
    restart(Panel.SCREEN_WIDTH, Panel.SCREEN_HEIGHT);
});
resolutionMenuItem7.addActionListener(e -> {
    Panel.SCREEN_WIDTH = 2560;
    Panel.SCREEN_HEIGHT = 1380;
    restart(Panel.SCREEN_WIDTH, Panel.SCREEN_HEIGHT);
});
resolutionMenuItem8.addActionListener(e -> {
    Panel.SCREEN_WIDTH = 3840;
    Panel.SCREEN_HEIGHT = 2100;
    restart(Panel.SCREEN_WIDTH, Panel.SCREEN_HEIGHT);
});
bgmMenuItem1.addActionListener(e -> {
    bgm.start();
    bgm.loop(Clip.LOOP_CONTINUOUSLY);
});
bgmMenuItem2.addActionListener(e -> bgm.stop());

最后别忘了将MenuBar绑定到Frame上。

//binding main bar
this.setJMenuBar(menuBar);

背景音乐

首先需要创建全局变量,方便随时控制背景音乐。

static Clip bgm;

接着调用音频文件,尝试播放,并设置循环播放。特别需要注意的是,此处的音频文件需要填写相对地址,方便后续打包发布。

File bgmPath = new File("bgm.wav");
try {
     AudioInputStream aIS = AudioSystem.getAudioInputStream(bgmPath);
     bgm = AudioSystem.getClip();
     bgm.open(aIS);
     bgm.start();
     bgm.loop(Clip.LOOP_CONTINUOUSLY);
 }
 catch (Exception ex){
     ex.printStackTrace();
 }

restart方法

将新的分辨率传入,设置Frame的新大小,最后模拟R键输入,重启游戏。

//set new resolution with restart the game and resize the frame
public void restart(int SCREEN_WIDTH, int SCREEN_HEIGHT){
    this.setSize(SCREEN_WIDTH + 15, SCREEN_HEIGHT + 60);
    //restart by Key_R hit in panel
    try {
        Robot robot = new Robot();
        robot.keyPress(KeyEvent.VK_R);
        robot.keyRelease(KeyEvent.VK_R);
    }
    catch (AWTException eA) {
        eA.printStackTrace();
    }
}

修改图标

和背景音乐类似,需要填写图片的相对路径,尝试获取文件资源后,绑定到Frame上。

BufferedImage image = null;
try {
    image = ImageIO.read(this.getClass()
            .getResource("./icon.png"));
} catch (IOException e) {
    e.printStackTrace();
}
this.setIconImage(image);

可以改进的地方

这次仍然时间很紧张,整体开发流程比较简单,没有计算器那样复杂的纠错检错,仅仅在美观与实用性上做了改进。后续可以在游戏性和可玩性上进行优化,例如引入多线程,开发多条蛇的联机;生成不同的食物,给予不同的效果;设计不同的障碍物、墙壁,允许蛇穿墙而过等等。

详细代码

本项目的全部源码以及打包程序均上传至GitHub,并附有说明文档,具体信息请前往Github查看,测试程序请前往Release下载,项目遵循MIT协议。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值