《Windows游戏编程大师技巧》(第二版)第1章(下)

实例:FreakOut
在沉溺于所讨论的有关Windows、DirectX 和3D 图形之前,应当暂停一下,先给你看一个完整的游戏——虽然简单了一点,但毫无疑问是一个完整的游戏。你会看到一个实际的游戏循环和一些图形功能调用,最后一霎那就可以通过编译。不错吧?跟我来吧!
问题是我们现在才讲到第一章。我不应该使用后面章节中的内容……这有点像作弊,对吧?因此,我决定要做的是让你习惯于使用黑黑(black box)API来进行游戏编程。基于这个要求,我要提一个问题“要制作一个类似Breakout(打砖块)的2D游戏,其最低要求是什么?”我们真正所需要的是下面的功能:
• 在任意图像模式中切换
• 在屏幕上画各种颜色的矩形
• 获取键盘输入
• 使用一些定时函数同步游戏循环
• 在屏幕上画彩色文字串
因此我建了一个名为BLACKBOX.CPP|H的库。它封装了一套DirectX函数集(限于DirectDraw),并且包含实现所需功能的支持代码。妙处是,读者根本不需要看这些代码,只需依照函数原型来使用这些函数就可以了,并与BLACKBOX.CPP|H连接来产生.EXE可执行文件。
以BLACKBOX库为基础,我编写了一个名字为FreakOut的游戏,这个游戏演示了本章中所讨论的许多概念。FreakOut 游戏包含真正游戏的全部主要组成部分,包括:游戏循环、计分、关卡,甚至还有为球而写的迷你物理模型。真是可爱。图1.9 是一幅游戏运行中的屏幕画面。显然它比不上Arkanoid(经典的打砖块类游戏),但4 个小时的工作有此成果也不赖!
图1.9 FreakOut游戏的截屏

在阅读游戏源代码之前,我希望读者能看一下工程和游戏各组成部分是如何协调一致的。参见图1.10。
图1.10 FreakOut 的结构

从图中可以看到,游戏由下面文件构成:
FREAKOUT.CPP——游戏的主要逻辑,使用BLACKBOX.CPP,创建一个最小化的Win32应用程序。
BLACKBOX.CPP——游戏库(请不要偷看:)。
BLACKBOX.H——游戏库的头文件。
DDRAW.LIB——用于生成应用程序的DirectDraw输入库。其中并不含有真正的DirectX代码。它主要是用作让用户调用的中间库,然后轮流调用进行实际工作的DDRAW.DLL动态链接库。它可以在DirectX SDK 安装目录下的LIB子目录内被找到。
DDRAW.DLL——运行时(Runtime)的DirectDraw 库,实际上含有通过DDRAW.LIB 输入库调用DirectDraw 接口函数的COM 执行程序。不必为此担心;只要确认已经安装了DirectX运行时文件即可。
为了通过编译,需要将BLACKBOX.CPP和FREAKOUT.CPP加入工程里面,连接上DDRAW.LIB库文件,并确保BLACKBOX.H在头文件搜索路径或工作目录里,以便编译器可以正确地找到它。
现在我们已大致了解了FreakOut的结构。让我们看一下BLACKOUT.H头文件,看看它包含了哪些函数。
程序清单1.2 BLACKOUT.H 头文件
// BLACKBOX.H - Header file for demo game engine library

// watch for multiple inclusions
#ifndef BLACKBOX
#define BLACKBOX

// DEFINES

// default screen size
#define SCREEN_WIDTH    640  // size of screen
#define SCREEN_HEIGHT   480
#define SCREEN_BPP      8    // bits per pixel
#define MAX_COLORS      256  // maximum colors

// MACROS /

// these read the keyboard asynchronously
#define KEY_DOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0)
#define KEY_UP(vk_code)   ((GetAsyncKeyState(vk_code) & 0x8000) ? 0 : 1)

// initializes a direct draw struct
#define DD_INIT_STRUCT(ddstruct) {memset(&ddstruct,0,sizeof(ddstruct));
ddstruct.dwSize=sizeof(ddstruct); }

// TYPES //

// basic unsigned types
typedef unsigned short USHORT;
typedef unsigned short WORD;
typedef unsigned char  UCHAR;
typedef unsigned char  BYTE;

// EXTERNALS //

extern LPDIRECTDRAW7         lpdd;             // dd object
extern LPDIRECTDRAWSURFACE7  lpddsprimary;     // dd primary surface
extern LPDIRECTDRAWSURFACE7  lpddsback;        // dd back surface
extern LPDIRECTDRAWPALETTE   lpddpal;          // a pointer dd palette
extern LPDIRECTDRAWCLIPPER   lpddclipper;      // dd clipper
extern PALETTEENTRY          palette[256];     // color palette
extern PALETTEENTRY          save_palette[256]; // used to save palettes
extern DDSURFACEDESC2        ddsd;    // a ddraw surface description struct
extern DDBLTFX               ddbltfx;           // used to fill
extern DDSCAPS2 ddscaps; // a ddraw surface capabilities struct
extern HRESULT               ddrval;            // result back from dd calls
extern DWORD                 start_clock_count; // used for timing
// these defined the general clipping rectangle
extern int min_clip_x,                             // clipping rectangle
           max_clip_x,
           min_clip_y,
           max_clip_y;

// these are overwritten globally by DD_Init()
extern int screen_width,                            // width of screen
           screen_height,                           // height of screen
           screen_bpp;                              // bits per pixel

// PROTOTYPES /

// DirectDraw functions
int DD_Init(int width, int height, int bpp);
int DD_Shutdown(void);
LPDIRECTDRAWCLIPPER DD_Attach_Clipper(LPDIRECTDRAWSURFACE7 lpdds,
                                      int num_rects, LPRECT clip_list);
int DD_Flip(void);
int DD_Fill_Surface(LPDIRECTDRAWSURFACE7 lpdds,int color);

// general utility functions
DWORD Start_Clock(void);
DWORD Get_Clock(void);
DWORD Wait_Clock(DWORD count);

// graphics functions
int Draw_Rectangle(int x1, int y1, int x2, int y2,
                   int color,LPDIRECTDRAWSURFACE7 lpdds=lpddsback);

// gdi functions
int Draw_Text_GDI(char *text, int x,int y,COLORREF color,
                  LPDIRECTDRAWSURFACE7 lpdds=lpddsback);
int Draw_Text_GDI(char *text, int x,int y,int color,
                  LPDIRECTDRAWSURFACE7 lpdds=lpddsback);

#endif
现在,不要花费太多时间绞尽脑汁研究这里的程序代码,搞清楚那些神秘的全局变量究竟表示什么并不重要。让我们来看一看这些函数。如你所想,这里有实现我们的简单图形界面所需的全部函数。基于这个图形界面和最小化的Win32 应用程序(我们要做的Windows 编程工作越少越好)的基础上,我创建了游戏FREAKOUT.CPP,如清单1.3 所示。请认真地看一看,尤其是游戏主循环和对游戏处理功能的调用。
程序清单1.3 FREAKOUT.CPP 源文件

// INCLUDES ///

#define WIN32_LEAN_AND_MEAN // include all macros
#define INITGUID            // include all GUIDs

#include <windows.h>        // include important windows stuff
#include <windowsx.h>
#include <mmsystem.h>

#include <iostream.h>       // include important C/C++ stuff
#include <conio.h>
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <string.h>
#include <stdarg.h>
#include <stdio.h>
#include <math.h>
#include <io.h>
#include <fcntl.h>

#include <ddraw.h>          // directX includes
#include "blackbox.h"       // game library includes

// DEFINES

// defines for windows
#define WINDOW_CLASS_NAME "WIN3DCLASS"  // class name

#define WINDOW_WIDTH            640     // size of window
#define WINDOW_HEIGHT           480

// states for game loop
#define GAME_STATE_INIT         0
#define GAME_STATE_START_LEVEL  1
#define GAME_STATE_RUN          2
#define GAME_STATE_SHUTDOWN     3
#define GAME_STATE_EXIT         4

// block defines
#define NUM_BLOCK_ROWS          6
#define NUM_BLOCK_COLUMNS       8

#define BLOCK_WIDTH             64
#define BLOCK_HEIGHT            16
#define BLOCK_ORIGIN_X          8
#define BLOCK_ORIGIN_Y          8
#define BLOCK_X_GAP             80
#define BLOCK_Y_GAP             32

// paddle defines
#define PADDLE_START_X          (SCREEN_WIDTH/2 - 16)
#define PADDLE_START_Y          (SCREEN_HEIGHT - 32);
#define PADDLE_WIDTH            32
#define PADDLE_HEIGHT           8
#define PADDLE_COLOR            191

// ball defines
#define BALL_START_Y            (SCREEN_HEIGHT/2)
#define BALL_SIZE                4

// PROTOTYPES /

// game console
int Game_Init(void *parms=NULL);
int Game_Shutdown(void *parms=NULL);
int Game_Main(void *parms=NULL);

// GLOBALS

HWND main_window_handle  = NULL; // save the window handle
HINSTANCE main_instance  = NULL; // save the instance
int game_state           = GAME_STATE_INIT; // starting state

int paddle_x = 0, paddle_y = 0; // tracks position of paddle
int ball_x   = 0, ball_y   = 0; // tracks position of ball
int ball_dx  = 0, ball_dy  = 0; // velocity of ball
int score    = 0;               // the score
int level    = 1;               // the current level
int blocks_hit = 0;             // tracks number of blocks hit

// this contains the game grid data

UCHAR blocks[NUM_BLOCK_ROWS][NUM_BLOCK_COLUMNS];

// FUNCTIONS //

LRESULT CALLBACK WindowProc(HWND hwnd,
                      UINT msg,
                            WPARAM wparam,
                            LPARAM lparam)
{
// this is the main message handler of the system
PAINTSTRUCT    ps;           // used in WM_PAINT
HDC            hdc;       // handle to a device context

// what is the message
switch(msg)
    {
    case WM_CREATE:
        {
    // do initialization stuff here
    return(0);
    }  break;

    case WM_PAINT:
         {
         // start painting
         hdc = BeginPaint(hwnd,&ps);

         // the window is now validated

         // end painting
         EndPaint(hwnd,&ps);
         return(0);
        }  break;

    case WM_DESTROY:
         {
           // kill the application
         PostQuitMessage(0);
         return(0);
           }  break;

    default:break;

    }  // end switch

// process any messages that we didn't take care of
return (DefWindowProc(hwnd, msg, wparam, lparam));

}  // end WinProc

// WINMAIN

int WINAPI WinMain(HINSTANCE hinstance,
            HINSTANCE hprevinstance,
            LPSTR lpcmdline,
            int ncmdshow)
{
// this is the winmain function

WNDCLASS winclass;  // this will hold the class we create
HWND     hwnd;         // generic window handle
MSG     msg;         // generic message
HDC      hdc;       // generic dc
PAINTSTRUCT ps;     // generic paintstruct
// first fill in the window class structure
winclass.style    = CS_DBLCLKS | CS_OWNDC |
                 CS_HREDRAW | CS_VREDRAW;
winclass.lpfnWndProc = WindowProc;
winclass.cbClsExtra        = 0;
winclass.cbWndExtra        = 0;
winclass.hInstance        = hinstance;
winclass.hIcon            = LoadIcon(NULL, IDI_APPLICATION);
winclass.hCursor        = LoadCursor(NULL, IDC_ARROW);
winclass.hbrBackground    = (HBRUSH)GetStockObject(BLACK_BRUSH);
winclass.lpszMenuName    = NULL;
winclass.lpszClassName    = WINDOW_CLASS_NAME;

// register the window class
if (!RegisterClass(&winclass))
    return(0);

// create the window, note the use of WS_POPUP
if (!(hwnd = CreateWindow(WINDOW_CLASS_NAME,    // class
        "WIN3D Game Console",    // title
        WS_POPUP | WS_VISIBLE,
        0,0,                    // initial x,y
        GetSystemMetrics(SM_CXSCREEN),  // initial width
        GetSystemMetrics(SM_CYSCREEN),  // initial height
        NULL,        // handle to parent
        NULL,        // handle to menu
        hinstance,   // instance
        NULL)))      // creation parms
return(0);

// hide mouse
ShowCursor(FALSE);

// save the window handle and instance in a global
main_window_handle = hwnd;
main_instance      = hinstance;

// perform all game console specific initialization
Game_Init();

// enter main event loop
while(1)
    {
    if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
    {
    // test if this is a quit
        if (msg.message == WM_QUIT)
           break;

    // translate any accelerator keys
    TranslateMessage(&msg);

    // send the message to the window proc
    DispatchMessage(&msg);
    } // end if
       // main game processing goes here
       Game_Main();

    }  // end while

// shutdown game and release all resources
Game_Shutdown();

// show mouse
ShowCursor(TRUE);

// return to Windows like this
return(msg.wParam);

}  // end WinMain

// T3DX GAME PROGRAMMING CONSOLE FUNCTIONS

int Game_Init(void *parms)
{
// this function is where you do all the initialization
// for your game

// return success
return(1);

}  // end Game_Init

///

int Game_Shutdown(void *parms)
{
// this function is where you shutdown your game and
// release all resources that you allocated

// return success
return(1);

}  // end Game_Shutdown

///

void Init_Blocks(void)
{
// initialize the block field
for (int row=0; row < NUM_BLOCK_ROWS; row++)
    for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
         blocks[row][col] = row*16+col*3+16;

}  // end Init_Blocks
///

void Draw_Blocks(void)
{
// this function draws all the blocks in row major form
int x1 = BLOCK_ORIGIN_X, // used to track current position
    y1 = BLOCK_ORIGIN_Y;

// draw all the blocks
for (int row=0; row < NUM_BLOCK_ROWS; row++)
    {
    // reset column position
    x1 = BLOCK_ORIGIN_X;

    // draw this row of blocks
    for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
        {
        // draw next block (if there is one)
        if (blocks[row][col]!=0)
            {
            // draw block
            Draw_Rectangle(x1-4,y1+4,
                 x1+BLOCK_WIDTH-4,y1+BLOCK_HEIGHT+4,0);

            Draw_Rectangle(x1,y1,x1+BLOCK_WIDTH,
                 y1+BLOCK_HEIGHT,blocks[row][col]);
            }  // end if

        // advance column position
        x1+=BLOCK_X_GAP;
        }  // end for col

    // advance to next row position
    y1+=BLOCK_Y_GAP;

    }  // end for row

}  // end Draw_Blocks

///

void Process_Ball(void)
{
// this function tests if the ball has hit a block or the paddle
// if so, the ball is bounced and the block is removed from
// the playfield note: very cheesy collision algorithm :)

// first test for ball block collisions

// the algorithm basically tests the ball against each
// block's bounding box this is inefficient, but easy to
// implement, later we'll see a better way

int x1 = BLOCK_ORIGIN_X, // current rendering position
    y1 = BLOCK_ORIGIN_Y;

int ball_cx = ball_x+(BALL_SIZE/2),  // computer center of ball
    ball_cy = ball_y+(BALL_SIZE/2);

// test of the ball has hit the paddle
if (ball_y > (SCREEN_HEIGHT/2) && ball_dy > 0)
   {
   // extract leading edge of ball
   int x = ball_x+(BALL_SIZE/2);
   int y = ball_y+(BALL_SIZE/2);

   // test for collision with paddle
   if ((x >= paddle_x && x <= paddle_x+PADDLE_WIDTH) &&
       (y >= paddle_y && y <= paddle_y+PADDLE_HEIGHT))
       {
       // reflect ball
       ball_dy=-ball_dy;

       // push ball out of paddle since it made contact
       ball_y+=ball_dy;

       // add a little english to ball based on motion of paddle
       if (KEY_DOWN(VK_RIGHT))
          ball_dx-=(rand()%3);
       else
       if (KEY_DOWN(VK_LEFT))
          ball_dx+=(rand()%3);
       else
          ball_dx+=(-1+rand()%3);

       // test if there are no blocks, if so send a message
       // to game loop to start another level
       if (blocks_hit >= (NUM_BLOCK_ROWS*NUM_BLOCK_COLUMNS))
          {
          game_state = GAME_STATE_START_LEVEL;
          level++;
          }  // end if

       // make a little noise
       MessageBeep(MB_OK);

       // return
       return;

       }  // end if

   }  // end if
// now scan thru all the blocks and see if ball hit blocks
for (int row=0; row < NUM_BLOCK_ROWS; row++)
    {
    // reset column position
    x1 = BLOCK_ORIGIN_X;

    // scan this row of blocks
    for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
        {
        // if there is a block here then test it against ball
        if (blocks[row][col]!=0)
           {
           // test ball against bounding box of block
           if ((ball_cx > x1) && (ball_cx < x1+BLOCK_WIDTH) &&
               (ball_cy > y1) && (ball_cy < y1+BLOCK_HEIGHT))
               {
               // remove the block
               blocks[row][col] = 0;

               // increment global block counter, so we know
               // when to start another level up
               blocks_hit++;

               // bounce the ball
               ball_dy=-ball_dy;

               // add a little english
               ball_dx+=(-1+rand()%3);

               // make a little noise
               MessageBeep(MB_OK);

               // add some points
               score+=5*(level+(abs(ball_dx)));

               // that's it -- no more block
               return;

               }  // end if

           }  // end if

        // advance column position
        x1+=BLOCK_X_GAP;
        }  // end for col

    // advance to next row position
    y1+=BLOCK_Y_GAP;

    }  // end for row
}  // end Process_Ball

///

int Game_Main(void *parms)
{
// this is the workhorse of your game it will be called
// continuously in real-time this is like main() in C
// all the calls for your game go here!

char buffer[80]; // used to print text

// what state is the game in?
if (game_state == GAME_STATE_INIT)
    {
    // initialize everything here graphics
    DD_Init(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP);

    // seed the random number generator
    // so game is different each play
    srand(Start_Clock());

    // set the paddle position here to the middle bottom
    paddle_x = PADDLE_START_X;
    paddle_y = PADDLE_START_Y;

    // set ball position and velocity
    ball_x = 8+rand()%(SCREEN_WIDTH-16);
    ball_y = BALL_START_Y;
    ball_dx = -4 + rand()%(8+1);
    ball_dy = 6 + rand()%2;

    // transition to start level state
    game_state = GAME_STATE_START_LEVEL;

    }  // end if

else
if (game_state == GAME_STATE_START_LEVEL)
    {
    // get a new level ready to run

    // initialize the blocks
    Init_Blocks();

    // reset block counter
    blocks_hit = 0;

    // transition to run state
    game_state = GAME_STATE_RUN;
    }  // end if
///
else
if (game_state == GAME_STATE_RUN)
    {
    // start the timing clock
    Start_Clock();

    // clear drawing surface for the next frame of animation
    Draw_Rectangle(0,0,SCREEN_WIDTH-1, SCREEN_HEIGHT-1,200);

    // move the paddle
    if (KEY_DOWN(VK_RIGHT))
       {
       // move paddle to right
       paddle_x+=8;

       // make sure paddle doesn't go off screen
       if (paddle_x > (SCREEN_WIDTH-PADDLE_WIDTH))
          paddle_x = SCREEN_WIDTH-PADDLE_WIDTH;

       }  // end if
    else
    if (KEY_DOWN(VK_LEFT))
       {
       // move paddle to right
       paddle_x-=8;

       // make sure paddle doesn't go off screen
       if (paddle_x < 0)
          paddle_x = 0;

       }  // end if

    // draw blocks
    Draw_Blocks();

    // move the ball
    ball_x+=ball_dx;
    ball_y+=ball_dy;

    // keep ball on screen, if the ball hits the edge of
    // screen then bounce it by reflecting its velocity
    if (ball_x > (SCREEN_WIDTH - BALL_SIZE) || ball_x < 0)
       {
       // reflect x-axis velocity
       ball_dx=-ball_dx;

       // update position
       ball_x+=ball_dx;
       }  // end if
    // now y-axis
    if (ball_y < 0)
       {
       // reflect y-axis velocity
       ball_dy=-ball_dy;

       // update position
       ball_y+=ball_dy;
       }  // end if
   else
   // penalize player for missing the ball
   if (ball_y > (SCREEN_HEIGHT - BALL_SIZE))
       {
       // reflect y-axis velocity
       ball_dy=-ball_dy;

       // update position
       ball_y+=ball_dy;

       // minus the score
       score-=100;

       }  // end if

    // next watch out for ball velocity getting out of hand
    if (ball_dx > 8) ball_dx = 8;
    else
    if (ball_dx < -8) ball_dx = -8;

    // test if ball hit any blocks or the paddle
    Process_Ball();

    // draw the paddle and shadow
    Draw_Rectangle(paddle_x-8, paddle_y+8,
                   paddle_x+PADDLE_WIDTH-8,
                   paddle_y+PADDLE_HEIGHT+8,0);

    Draw_Rectangle(paddle_x, paddle_y,
                   paddle_x+PADDLE_WIDTH,
                   paddle_y+PADDLE_HEIGHT,PADDLE_COLOR);

    // draw the ball
    Draw_Rectangle(ball_x-4, ball_y+4, ball_x+BALL_SIZE-4,
                   ball_y+BALL_SIZE+4, 0);
    Draw_Rectangle(ball_x, ball_y, ball_x+BALL_SIZE,
                   ball_y+BALL_SIZE, 255);

    // draw the info
    sprintf(buffer,"F R E A K O U T           Score %d   //
          Level %d",score,level);
    Draw_Text_GDI(buffer, 8,SCREEN_HEIGHT-16, 127);
    // flip the surfaces
    DD_Flip();

    // sync to 33ish fps
    Wait_Clock(30);

    // check if user is trying to exit
    if (KEY_DOWN(VK_ESCAPE))
       {
       // send message to windows to exit
       PostMessage(main_window_handle, WM_DESTROY,0,0);

       // set exit state
       game_state = GAME_STATE_SHUTDOWN;

       }  // end if

    }  // end if
///
else
if (game_state == GAME_STATE_SHUTDOWN)
   {
   // in this state shut everything down and release resources
   DD_Shutdown();

   // switch to exit state
   game_state = GAME_STATE_EXIT;

   }  // end if

// return success
return(1);

}  // end Game_Main
哈哈,酷吧?这就是一个完整的Win32/DirectX游戏了,至少几乎是完整的了。BLACKOUT.CPP源文件中有好几百行代码,但是我们可以将其视为某人(我!)编写的DirectX的一部分。不管怎样说,还是让我们迅速浏览一下程序清单1.3的内容吧。
首先,Windows 需要一个事件循环。这是所有Windows程序的标准结构,因为Windows几乎完全是事件驱动的。但是游戏却不是事件驱动的,无论用户在干什么,它们都在一直运行。因此,我们至少需要支持小型事件循环以配合Windows。执行这项功能的代码位于WinMain()中。WinMain() 是所有Windows 程序的主要入口点,就好比main()是所有DOS/UNIX 程序中的入口点一样。FreakOut 的WinMain()创建一个窗口并进入事件循环。当Windows需要作某些工作时,就随它去。当所有的基本事件处理都结束时,调用Game_Main()。Game_Main是实际运行游戏程序的部分。
如果愿意的话,你可以不停地在Game_Main()中循环,而不释放回到WinMain()主事件循环体中。但这样做不是件好事,因为Windows会得不到任何信息。哎,我们该做的是让游戏在运行一帧时间的动画和逻辑之后,返回到WinMain()。这样的话,Windows可以继续响应和处理信息。如果所有这些听起来像是幻术的话,请不要担心——在下一章中情况还会更糟。
进入Game_Main()后,FreakOut的游戏逻辑开始被执行。游戏图像被渲染到一个不直接显示出来的工作缓冲区,尔后通过调用DD_FLIP()而在循环结束时在显示屏上显示出来。因此我希望你阅读一下全部的游戏状态,一行一行地过一遍一遍游戏循环的每一部分,了解工作原理。要启动游戏,只须双击FREAKOUT.EXE,游戏程序会立即启动。游戏控制方式如下:
右箭头键——向右移动挡板。
左箭头键——向左移动挡板。
Esc键——退回Windows。
还有,如果你错过一个球的话,将被罚掉100分,可要仔细盯紧啊!
如果你已经明白了游戏代码和玩法,不妨试着修改一下游戏。你可以增加不同的背景颜色(0~255 是有效的颜色)、增加更多的球、可以改变挡板的大小以及加上更多的声音效果(目前我只用到了Win32 API 中的MessageBeep()函数)

 


 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值