《Windows游戏编程大师技巧》第六章:初次邂逅DirectDraw

1、DirectX错误处理

通过上一章知道DirectDrawCreate函数可以创建一个DirectDraw COM对象,并得到一个接口指针:

LPDIRECTDRAW  lpdd = NULL;
DirectDrawCreate(NULL, &lpdd, NULL); 

可以用宏FAILED检测函数是否执行失败,宏SUCCEEDED()来检测函数是否执行成功,eg:

LPDIRECTDRAW  lpdd = NULL;
if(FAILED(DirectDrawCreate(NULL, &lpdd, NULL)))
{
	//failed
	...
	return;
}
//succeeded
...

LPDIRECTDRAW  lpdd = NULL;
if(SUCCEEDED(DirectDrawCreate(NULL, &lpdd, NULL)))
{
	//succeeded
	...
}
else
{
	//failed
	...
}
也可以通过 DirectDrawCreate()的返回值来判断是否出错。

2、DirectDraw接口

DirectDraw由五个接口组成:IUnknown、IDirectDraw、IDirectDrawSurface、IDirectDrawPalette、IDirectDrawClipper。

IUnknown:所有com接口都必须从它继承。

IDirectDraw:开始使用DirectDraw时必须创建的主接口对象。IDirectDraw一对一的表示视频卡及其支持硬件。

IDirectDrawSurface:用DirectDraw创建、控制和显示的实际显示表面。显示表面可以使用显存存与视频卡或系统内存中,分为主显示表面和从显示表面。主显示表面通常表示正在被视频卡光栅化和显示的实际视频缓冲区,而从显示表面通常是离屏的。通常需要创建单个的主显示表面以表示实际视频显示,创建一个或多个从显示表面以表示对象位图、后备缓冲以表示将构造下一帧动画的离屏绘图区。

IDirectDrawPalette:创建、加载和控制调色板,以及将调色板关联到显示表面,例如主显示表面和从显示表面。

IDirectDrawClipper:剪切光栅和位图操作到一些可见显示表面的子集。


3、设置协作级别

对DDraw来说,我们唯一关心的是视频显示设备,有两种情况:全屏模式和窗口模式。用IDirectDraw7::SetCooperativeLevel来设置DDraw的协作级别。声明如下:

  
  
HRESULT SetCooperativeLevel(HWND hWnd, // 窗口句柄,一般使用主窗口句柄 DWORD dwFlags); // 控制标志

dwFlags控制标志,它直接影响DDraw与Windows之间协作方式。

SetCooperativeLevel()的控制标记 

描述

DDSCL_ALLOWMODEX

允许使用Mode X (320x200,240,400) 显示模式.当设置DDSCL_EXCLUSIVE和DDSCL_FULLSCREEN时生效

DDSCL_ALLOWREBOOT

允许在排他(全屏)模式下检测到Ctrl+Alt+Del

DDSCL_EXCLUSIVE

请求排他级别。此标记需要与DDSCL_FULLSCREEN标记同时作用

DDSCL_FPUSETUP

表示调用 程序希望配置FPU以得到最佳的Direct3D性能(禁用度和异常),这样Direct3D不需要每次都明确地FPU。更多信息,请在DirectX SDK中查询“DDraw协作级别和FPU数度”

DDSCL_FULLSCREEN

表示使用全屏模式。其他应用程序的GDI将不能写屏,此标记必须与DDSCL_EXCLUSIVE标记同时使用。

DDSCL_MULTITHREADED

请求对于多线程安全的DDraw行为。

DDSCL_NORMAL

表示应用程序是一个通常的Windows应用程序。这个标记不能与DDSCL_ALLOWMODEX, DDSCL_EXCLUSIVE, 或 DDSCL_FULLSCREEN标记一起使用。

DDSCL_NOWINDOWCHANGES

表示不允许DDraw激活时最小化或还原应用程序窗口。

上面写了一大堆标记,其实大多数情况是下面两种情况。

全屏模式:DDSCL_FPUSETUP | DDSCL_ALLOWMODEX | DDSCL_EXCLUSIVE | DDSCL_ALLOWREBOOT

窗口模式:DDSCL_NORMAL

4 、设置显示(视频)模式

IDirectDraw7::SetDisplayMode()用来设置显示(视频)模式,函数原型:

HRESULT SetDisplayMode(
  DWORD dwWidth,  //宽
  DWORD dwHeight, //高
  DWORD dwBPP,    //色深,即每像素的位数
  DWORD dwRefreshRate, //刷新频率,0为使用默认值
  DWORD dwFlags//一般设为0
); 

例如,创建一个256色(8位)的640×480模式:

lpdd->SetDisplayMode(640, 480, 8, 0, 0);

一个简单的DirectX应用程序:

HWND main_window_handle = NULL;        // 主窗口句柄
LPDIRECTDRAW7 lpdd    = NULL;         // DDraw 对象

int Game_Init(void *parms = NULL, int num_parms = 0)
{
   // 创建ddraw对象
   if (FAILED(DirectDrawCreateEx(NULL, (void **)&lpdd, IID_IDirectDraw7, NULL)))
      return(0);

   // 设置协作级别
  if (FAILED(lpdd->SetCooperativeLevel(main_window_handle, 
                                      DDSCL_FULLSCREEN | DDSCL_ALLOWMODEX | 
                                      DDSCL_EXCLUSIVE | DDSCL_ALLOWREBOOT)))
       return(0);

  //设置视频模式
  if (FAILED(lpdd->SetDisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,0,0)))
      return(0);

   return 1;
}

/
int Game_Shutdown()
{
   if (lpdd)
   {
      lpdd->Release();
      lpdd = NULL;
   } 
   return 1;
}


int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd)
{
	//创建窗口
	......
	CreateWindow();
	
	Game_Init();

	//消息循环
    while(TRUE)
    {
	   if (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
	   { 
		  if (msg.message == WM_QUIT)
		    break;
		 ::TranslateMessage(&msg);
		 ::DispatchMessage(&msg);
	   }
    }

	Game_Shutdown();

	return msg.wParam;
}
5、创建调色板

调色板项结构如下:

typedef struct tagPALETTEENTRY {
  BYTE peRed;  //红色强度值
  BYTE peGreen;//绿色强度值
  BYTE peBlue; //蓝色强度值
  BYTE peFlags;//一般设为PC_NOCOLLAPSE
} PALETTEENTRY; 

调色板数据结构是一个包含256个调色板项的数组:

PALETTENTRY  palette[256];

以下代码创建一个随机调色板结构,并设置0位置为黑,255位置为白:

PALETTEENTRY          save_palette[256];     // used to save palettes

// build up the palette data array
for (int color=1; color < 255; color++)
{
    // fill with random RGB values
    palette[color].peRed   = rand()%256;
    palette[color].peGreen = rand()%256;
    palette[color].peBlue  = rand()%256;

    // set flags field to PC_NOCOLLAPSE
    palette[color].peFlags = PC_NOCOLLAPSE;
} // end for color

// now fill in entry 0 and 255 with black and white
palette[0].peRed   = 0;
palette[0].peGreen = 0;
palette[0].peBlue  = 0;
palette[0].peFlags = PC_NOCOLLAPSE;

palette[255].peRed   = 255;
palette[255].peGreen = 255;
palette[255].peBlue  = 255;
palette[255].peFlags = PC_NOCOLLAPSE;
IDirectDraw7::CreatePalette()函数用来创建调色板对象,获得调色板接口,函数原型:

HRESULT CreatePalette( 
  DWORD dwFlags, //控制标记
  LPPALETTEENTRY lpDDColorArray, //初始调色板,可以为NULL
  LPDIRECTDRAWPALETTE FAR* lplpDDPalette, //获得的IDirectDrawPalette接口指针
  IUnknown FAR* pUnkOuter//一般设为NULL
);
对于8位调色板,控制标记dsFlags一般设为DDPCAPS_8BIT | DDPCAPS_ALLOW256 | DDPCAPS_INITIALIZE。

以下为利用上面的随机调色板获得调色板对象:

LPDIRECTDRAWPALETTE   lpddpal      = NULL;   // a pointer to the created dd palette
// create the palette object
if (FAILED(lpdd->CreatePalette(DDPCAPS_8BIT | DDPCAPS_ALLOW256 | 
                                DDPCAPS_INITIALIZE, 
                                palette,&lpddpal, NULL)))
{
// error
return(0);
} // end if
6、创建显示表面

IDirectDraw7::CreateSurface()用来创建显示表面,获得显示表面接口,函数原型:

HRESULT CreateSurface(
  LPDDSURFACEDESC2 lpDDSurfaceDesc2, //DDSURFACEDESC2数据结构,用来描述希望创建的显示表面
  LPDIRECTDRAWSURFACE4 FAR* lplpDDSurface, //用来接收IDirectDrawSurface接口指针
  IUnknown FAR* pUnkOuter //一般设为NULL
);
以下为创建一个主显示表面的代码:

LPDIRECTDRAWSURFACE7  lpddsprimary = NULL;   // dd primary surface
DDSURFACEDESC2        ddsd;                  // a direct draw surface description struct

// clear ddsd and set size
memset(&ddsd,0,sizeof(ddsd)); 
ddsd.dwSize = sizeof(ddsd);
// enable valid fields
ddsd.dwFlags = DDSD_CAPS;
// request primary surface
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;//显示表面是主显示表面
//ddsd.width和ddsd.height等不必设置,会默认使用SetDisplayMode()中的设置

 // create the primary surface
if (FAILED(lpdd->CreateSurface(&ddsd, &lpddsprimary, NULL)))
{
   // error
   return(0);
} // end if
7、关联调色板到显示表面

IDirectDrawSurface7::SetPalette()用来关联调色板到显示表面,函数原型:

HRESULT SetPalette(LPDIRECTDRAWPALETTE* lpDDPalette);

参数lpDDPaletteDirectDraw7::CreatePalette()函数获得的调色板接口指针。

8、绘制像素

DirectDraw视频模式和显示表面都是线性的,当从一行移到另一行时,内存从左到右、从上到下增长。

要定位显示表面的任何位置,需要每排的内存间距(每行的字节数)和每个像素的大小(8位、16位、24位、32位),内存间距可以通过显示表面加锁lock()函数的第二个参数获得。可以使用下面的公式:

video_buffer_8[x+y*mempitch] = pixel_color_8;

video_buffer_16[x+y*(mempitch>>1)] = pixel_color_16;

其中video_buffer为指向显示表面内存的的指针(8位或16位模式),pixel_color为颜色值(8位或16位)。8位的颜色值可以直接用一个UCHAR来表示,而16位的颜色值通常编码成一个R5G6B5R5G5B5(一位弃用),可以用下面的宏来表示一个16位的RGB颜色值:

#define  _RGB16BIT565(r, g, b)   ((b&31) + ((g&63<<5)) + ((r&31)<<11))

#define  _RGB16BIT555(r, g, b)   ((b&31) + ((g&31)<<5) + ((r&31)<<10))

要访问任何显示表面,必须对内存加锁和解锁,IDirectDrawSurface7::Lock()用来加锁,函数原型:

HRESULT Lock(LPRECT lpDestRect,  //显示表面中要上锁的区域,NULL为整个表面上锁
                             LPDDSURFACEDESC2 lpDDSurfaceDesc,//DDSURFACEDESC2的地址,获得显示表面的信息
                             DWORD dwFlags,//控制标记
                             HANDLE hEvent);//高级特性,一般设为NULL
参数dwFlags的取值及意义:

参数DDSurfaceDesc结构的成员lPitch为每行的内存间距,成员lpSurface为指向显示表面的指针。

对显示表面解锁使用IDirectDrawSurface7::UnLock(LPRECT lpRect),如果是整个显示表面则传NULL。

9、清理资源

使用完DirectX对象后要Release()它们,而且应该按照创建时相反的顺序来Release(),eg:

<span style="font-size:14px;">// first the palette
if (lpddpal)
   {
   lpddpal->Release();
   lpddpal = NULL;
   } // end if

// now the primary surface
if (lpddsprimary)
   {
   lpddsprimary->Release();
   lpddsprimary = NULL;
   } // end if

// now blow away the IDirectDraw4 interface
if (lpdd)
   {
   lpdd->Release();
   lpdd = NULL;
   } // end if</span>

10、完整代码示例

综上所述,要在全屏的DirectDraw模式下绘制像素:必须建立DirectDraw对象以获得接口,设置协作级别,设置显示(视频)模式,创建调色板和显示表面并关联调色板到显示表面,然后在显示表面绘制像素,使用完后还要清理资源。完整示例代码如下:

#define WIN32_LEAN_AND_MEAN  // just say no to MFC
#define INITGUID // make sure directX guids are included

#include <windows.h>   
#include <ddraw.h> 
#pragma comment(lib,"ddraw.lib")

// DEFINES 
#define WINDOW_CLASS_NAME L"WINCLASS1"

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

// TYPES //
typedef unsigned short USHORT;
typedef unsigned short WORD;
typedef unsigned char  UCHAR;
typedef unsigned char  BYTE;

// MACROS /
#define KEYDOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0)
#define KEYUP(vk_code)   ((GetAsyncKeyState(vk_code) & 0x8000) ? 0 : 1)

#define DD_INIT_STRUCT(ddstruct) { memset(&ddstruct,0,sizeof(ddstruct)); ddstruct.dwSize=sizeof(ddstruct); }

// GLOBALS 
HWND      main_window_handle = NULL; // globally track main window
HINSTANCE hinstance_app      = NULL; // globally track hinstance

LPDIRECTDRAW7         lpdd         = NULL;   // dd object
LPDIRECTDRAWSURFACE7  lpddsprimary = NULL;   // dd primary surface
LPDIRECTDRAWSURFACE7  lpddsback    = NULL;   // dd back surface
LPDIRECTDRAWPALETTE   lpddpal      = NULL;   // a pointer to the created dd palette
LPDIRECTDRAWCLIPPER   lpddclipper  = NULL;   // dd clipper
PALETTEENTRY          palette[256];          // color palette
PALETTEENTRY          save_palette[256];     // used to save palettes
DDSURFACEDESC2        ddsd;                  // a direct draw surface description struct
DDBLTFX               ddbltfx;               // used to fill
DDSCAPS2              ddscaps;               // a direct draw surface capabilities struct
HRESULT               ddrval;                // result back from dd calls
DWORD                 start_clock_count = 0; // used for timing

int min_clip_x = 0,                          // clipping rectangle 
    max_clip_x = SCREEN_WIDTH-1,
    min_clip_y = 0,
    max_clip_y = SCREEN_HEIGHT-1;

int screen_width  = SCREEN_WIDTH,            // width of screen
    screen_height = SCREEN_HEIGHT,           // height of screen
    screen_bpp    = SCREEN_BPP;              // bits per pixel


char buffer[80];                     // general printing buffer


LRESULT CALLBACK WindowProc(HWND hwnd, 
						    UINT msg, 
                            WPARAM wparam, 
                            LPARAM lparam)
{
	PAINTSTRUCT		ps;		// used in WM_PAINT
	HDC				hdc;	// handle to a device context
	char buffer[80];        // used to print strings

	switch(msg)
	{	
		case WM_CREATE: 
			{
				// do initialization stuff here
				
				return(0);
			} 
			break;
		case WM_PAINT: 
			{
				// simply validate the window 
   				hdc = BeginPaint(hwnd,&ps);	 
		        
				// end painting
				EndPaint(hwnd,&ps);

				return(0);
   			} 
			break;
		case WM_DESTROY: 
			{
				// kill the application, this sends a WM_QUIT message 
				PostQuitMessage(0);

				return(0);
			} 
			break;

		default:break;
	} 

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

} 


int Game_Init(void *parms = NULL, int num_parms = 0)
{
	// this is called once after the initial window is created and
	// before the main event loop is entered, do all your initialization here

	//创建DirectDraw对象,获得IDirectDraw interface 7.0接口指针
	if (FAILED(DirectDrawCreateEx(NULL, (void **)&lpdd, IID_IDirectDraw7, NULL)))
	   return(0);

	//设置DirectDraw协作级别
	if (FAILED(lpdd->SetCooperativeLevel(main_window_handle, 
										  DDSCL_FULLSCREEN | DDSCL_ALLOWMODEX | 
										  DDSCL_EXCLUSIVE | DDSCL_ALLOWREBOOT)))
    {
	   return(0);
	} 

	//设置显示模式
	if (FAILED(lpdd->SetDisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,0,0)))
	{
	   return(0);
	} 

	//创建显示表面
	memset(&ddsd,0,sizeof(ddsd)); 
	ddsd.dwSize = sizeof(ddsd);
	ddsd.dwFlags = DDSD_CAPS;
	ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;
	if (FAILED(lpdd->CreateSurface(&ddsd, &lpddsprimary, NULL)))
	{
	   return(0);
	} 

	//创建一个随机调色板结构,设置0位置为黑,255位置为白
	for (int color=1; color < 255; color++)
	{
		// fill with random RGB values
		palette[color].peRed   = rand()%256;
		palette[color].peGreen = rand()%256;
		palette[color].peBlue  = rand()%256;
		palette[color].peFlags = PC_NOCOLLAPSE;
	} 
	// now fill in entry 0 and 255 with black and white
	palette[0].peRed   = 0;
	palette[0].peGreen = 0;
	palette[0].peBlue  = 0;
	palette[0].peFlags = PC_NOCOLLAPSE;

	palette[255].peRed   = 255;
	palette[255].peGreen = 255;
	palette[255].peBlue  = 255;
	palette[255].peFlags = PC_NOCOLLAPSE;

	//创建调色板对象,获得调色板接口
	LPDIRECTDRAWPALETTE   lpddpal      = NULL;   // a pointer to the created dd palette	
	if (FAILED(lpdd->CreatePalette(DDPCAPS_8BIT | DDPCAPS_ALLOW256 | 
									DDPCAPS_INITIALIZE, 
									palette,&lpddpal, NULL)))
	{
		return(0);
	} 

	//关联调色板到显示表面
	if (FAILED(lpddsprimary->SetPalette(lpddpal)))
	{
		return(0);
	} 

	return(1);
}


int Game_Main(void *parms = NULL, int num_parms = 0)
{
	// for now test if user is hitting ESC and send WM_CLOSE
	if (KEYDOWN(VK_ESCAPE))
	   SendMessage(main_window_handle,WM_CLOSE,0,0);

	//使用显示表面前要加锁
	memset(&ddsd,0,sizeof(ddsd)); 
	ddsd.dwSize = sizeof(ddsd);
	if (FAILED(lpddsprimary->Lock(NULL, &ddsd,
					   DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT,
					   NULL)))
	{
	   return(0);
	} 

	int mempitch        = (int)ddsd.lPitch;//每行内存间距
	UCHAR *video_buffer = (UCHAR *)ddsd.lpSurface;//指向显示表面的指针

	//绘制1000个随机像素到显示表面,颜色随机,立即可见
	for (int index=0; index < 1000; index++)
	{
		// select random position and color for 640x480x8
		UCHAR color = rand()%256;
		int x = rand()%640;
		int y = rand()%480;

		// plot the pixel
		video_buffer[x+y*mempitch] = color;        

	} 

	if (FAILED(lpddsprimary->Unlock(NULL)))
	   return(0);

	Sleep(30);

	return(1);
} 


int Game_Shutdown(void *parms = NULL, int num_parms = 0)
{
	//清理资源

	// first the palette
	if (lpddpal)
	{
	   lpddpal->Release();
	   lpddpal = NULL;
	}

	// now the primary surface
	if (lpddsprimary)
	{
	   lpddsprimary->Release();
	   lpddsprimary = NULL;
	} 

	// now blow away the IDirectDraw4 interface
	if (lpdd)
	{
	   lpdd->Release();
	   lpdd = NULL;
    } 

	return(1);
} 

// WINMAIN 

int WINAPI WinMain(	HINSTANCE hinstance,
					HINSTANCE hprevinstance,
					LPSTR lpcmdline,
					int ncmdshow)
{

	WNDCLASSEX winclass; // this will hold the class we create
	HWND	   hwnd;	 // generic window handle
	MSG		   msg;		 // generic message
	HDC        hdc;      // graphics device context

	// first fill in the window class stucture
	winclass.cbSize         = sizeof(WNDCLASSEX);
	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;
	winclass.hIconSm        = LoadIcon(NULL, IDI_APPLICATION);

	// save hinstance in global
	hinstance_app = hinstance;

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

	// create the window
	if (!(hwnd = CreateWindowEx(NULL,                  // extended style
								WINDOW_CLASS_NAME,     // class
								L"DirectDraw Full-Screen Demo", // title
								WS_POPUP | WS_VISIBLE,
					 			0,0,	  // initial x,y
								SCREEN_WIDTH,SCREEN_HEIGHT,  // initial width, height
								NULL,	  // handle to parent 
								NULL,	  // handle to menu
								hinstance,// instance of this application
								NULL)))	// extra creation parms
	return(0);

	// save main window handle
	main_window_handle = hwnd;

	// initialize game here
	Game_Init();

	// enter main event loop
	while(TRUE)
	{
		// test if there is a message in queue, if so get it
		if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
		{ 
		   if (msg.message == WM_QUIT)
			   break;
		  
		   TranslateMessage(&msg);
		   DispatchMessage(&msg);
		} 
	    
		// main game processing goes here
		Game_Main();       
	} 

	// closedown game here
	Game_Shutdown();

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

需要注意的地方有两点:一是CreateWindowEx()创建窗口的时候使用了WS_POPUP窗口风格,即去除一切控件和Windows GDI内容,而这正是全屏DirectX应用程序所需要的。

二是GetAsyncKeyState()函数的使用:GetAsyncKeyState(int vKey)确定用户是否按下了键盘上的一个键,参数为要检查键的虚键代码。返回值的最高位用来表示这个键是否按下(是为1,否为0),最低位表示在上次调用GetAsyncKeyState()后这个键是否被按下。
另一个函数GetKeyState与GetAsyncKeyState函数不同。GetAsyncKeyState在按下某键的同时调用,判断正在按下某键GetKeyState则在按过某键之后再调用,它返回最近的键盘消息从线程的队列中移出时的键盘状态,判断刚按过了某键。
SHORT GetKeyState(int nVirtKey);  
SHORT GetAsyncKeyState(int vKey);  
BOOL GetKeyboardState(PBYTE lpKeyState); 
三个取key status的函数的最大区别是:  
第一个:是从windows消息队列中取得键盘消息,返回key status.  
第二个:是直接侦测键盘的硬件中断,返回key status.  
第三个:是当从windows消息队列中移除键盘消息时,才返回key status



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值