游戏开发论坛

 找回密码
 立即注册
搜索
查看: 6824|回复: 7

周末写了点东西,大家帮忙看看

[复制链接]

89

主题

822

帖子

847

积分

高级会员

Rank: 4

积分
847
发表于 2005-7-3 17:40:00 | 显示全部楼层 |阅读模式
gc_img模块使用说明
一、概述

gc_img是一个简单游戏的框架模块,可以帮助很好的理解游戏程序的架构和工作方式,对于初学者来说是一个不可多得的参考材料,简化了图形方面的过于纷繁复杂的函数,把重点放在了资源的组织、游戏机本机制上。

 

二、编译环境

Microsoft DirectX 9.0C June 2005

Microsoft Visual Studio.NET 2003

使用如何检查自己的DirectX版本?

启动[开始]菜单-[运行]项,输入dxdiag,可以查看DirectX的相关信息,另外在Windows系统目录System32下会有一个d3dx9_26.dll文件,该文件是DirectX 9.0C June 2005版本所包含,如果没有该文件,证明安装的DirectX版本不是最新的,可以到微软的网站上去下载。使用gc_img模块不需要安装DirectX SDK,只要安装Runtime库即可,但是建议安装完整的SDK,可以学习到更多关于DirectX的知识。



更多关于DirectX的信息可以参看http://msdn.microsoft.com/directx/

 

三、知识准备

熟悉C++语法,重点函数指针,类,继承,派生,虚函数,虚类

熟悉VC编译环境

熟悉Win32应用程序

熟悉模版库和STL

下面简单介绍一下这几方面的知识

1、函数指针,类,继承,派生,虚函数,虚类

函数指针

函数指针就是一个指向函数的指针,我们对指向变量的指针应该很熟悉

int a = 10;

int * p = &a; // p是一个指向整型变量a的指针

当我们取p所指向的地址的值时,实际上就是访问a的值

b = *p; // 这里b就等于a

函数指针也一样

typedef int (*FUNC_PTR)( int ); // 这里我们定义了一个指向函数的指针类型

如果看不懂上面的定义方式,记住就可以了,上面只是定义了类型,定义了一个参数是整型返回值为整型的函数指针,只要是这样的函数,我们都可以用这个指针来指向它。

比如:

int Func1( int a )
{
    return a + 10;
}

int Func2( int a )
{
    return a * 10;
}

上面有两个函数,都符合我们定义的函数指针类型,那么我们定义2个函数指针来指向它

FUNC_PTR p1 = Func1;
FUNC_PTR p2 = Func2;

我们引用函数指针也就是调用了这2个函数,下面来看调用

int a = p1( 10 ); // a的值就是20
int b = (*p2)( 10 ); // b的值就是100

上面有2种方式调用,第一种是C++语法,第二种是C的语法,记住一种就好了

这就是关于函数指针的语法,那么函数指针有什么用呢,大家可以看到,我们在定义函数指针并不知道函数名叫什么,但是我们知道函数的参数和返回值,也就是说我们在不知道函数名的情况下可以调用一个函数,一般用于回调函数,事件处理等,拿游戏来说,引擎中有一个事件想通知外面的开发者,但是引擎写的时候并不知道开发者定义的函数名称,只要定义一个函数指针,调用这个函数指针就可以了,开发者在定义好了函数以后,把函数名(准确的说是函数地址)通过函数指针传递给引擎就可以了。

我们在Win32常常用到一个时钟函数SetTimer,最后一个参数就是函数指针,类型时TIMER_PROC,就是时间到了以后就调用这个参数指向的函数,从而通知我们时钟触发的事件。



类简单的说是一个具有成员变量和成员函数的自定义变量类型,就跟结构差不多(C++语法已经没有区别了),不必要理解的太复杂,其实类跟面向对象的编程没有直接的关系,不是说你使用了类就是面向对象的编程了,仅仅只是使用了一种C++语法而已,所以,如果对面向对象的编程不是很理解,并不影响对类的使用,就当它是一种结构好了。类的定义如下:

class A
{
private:
    int a;
protected:
    int b;
public:
    int c;
    A( ){ a = 0; }
    A( int v ){ a = v; }
    ~A( ) { }
};

这个定义中使用了3个成员访问限定符private,protected,public。private就是表示成员只能A内部访问,protected表示A和A的派生类可以访问,public表示所有的地方都可以访问。

A( )是构造函数,构造函数就是当我们定义一个A的变量是,它自动调用的一个函数,构造函数可以有多个,也可以没有,没有的话C++就自己调用隐藏的默认构造函数。

A a1; // a1中的变量a就成了0,自动调用了我们定义的A( )函数
A a2( 10 ); // a1中的变量a就成了10,自动调用了我们定义的A( int v )函数

我们也可以通过new来生成

A * p1 = new A( ); // p1中的变量a就成了0,自动调用了我们定义的A( )函数
A * p2 = new A( 10 ); // p2中的变量a就成了10,自动调用了我们定义的A( int v )函数

~A( )是析构函数,析构函数就是当类(实例)被释放的时候,它自动调用的一个函数,析构函数只能有一个或者没有。

if ( p1 ) delete p1; // 自动调用~A( )

构造函数和析构函数的函数名都和类的名字一样,并且不能有返回值,void也不能返回。

构造函数时候可以初始化一下变量,析构函数时候可以释放一下C++不能自动释放的内存。

我们还可以通过对构造函数和析构函数使用成员访问限定符,如果我们定义一个私有的构造函数,那么这个类就不能简单的定义实例了。

class B
{
    ~B( ){ }
public:
    B( ){ }
    void Release( ){ delete this; }
};

B * pb = new B( );

pb不能通过delete pb来释放了,因为不能调用~B( )函数,但我们可以通过pb->Release( )来释放内存。

继承和派生

继承和派生就主要目的就是我们可以重复利用已经实现的代码,

class A
{
private:
    int a;
protected:
    int b;
public:
    int c;
    A( ){ a = 0; }
    A( int v ){ a = v; }
    ~A( ) { }
};

class AA : public A
{
public:
    int func( int v ){ return v + 10; }
};

那么AA就是A的派生类,它继承了A的部分成员变量和成员函数(protected和public),但不能访问A里面private的成员。这些都容易理解,这里需要强调的是指针的转换

A * paa = new AA( );

((AA*)paa)->func( 10 );

这里我们将一个AA指针强制转换给了A类型指针paa,然后我们调用AA的成员函数的时候,又将其强制转换回来,这种转换我们认为是安全的,但也是危险的。看下面的例子

A * paa = new A( );

((AA*)paa)->func( 10 ); // 语法上没有错误,但是执行起来却是很危险的

C++中有一个cast操作符,可以查看MSDN关于这个操作符的相关说明。我们在使用的时候要特别注意,避免上面的错误。

 

虚函数

下面来说说C++类中最有活力的虚函数,如果没有派生类,虚函数没有任何意义,虚函数有点像函数指针的功能,我们来看看例子

class A
{
private:
    int a;
protected:
    int b;
public:
    int c;
    A( ){ a = 0; }
    A( int v ){ a = v; }
    ~A( ) { }

    virtual int func1( ){ return 1; }
    int func2( ){ return 2; }

    int test1( ) { return func1( ); }
    int test2( ) { return func2( ); }
};

class AA : public A
{
public:
    virtual int func1( ){ return 3; }
    int func2( ){ return 4; }

    int test3( ) { return func1( ); }
    int test4( ) { return func2( ); }
};

A v1;
AA v2;

int t1 = v1.test1( ); // t1 = 1
int t2 = v1.test2( ); // t2 = 2
int t3 = v2.test3( ); // t3 = 3
int t4 = v2.test4( ); // t4 = 4

上面的函数调用应该没什么问题,也没有什么疑问,下面是关键的

int t5 = v2.test1( ); // t5 = 3
int t6 = v2.test2( ); // t6 = 2

派生类AA调用基类A的函数test1, test2,基类的test1函数调用了虚函数func1,派生类的实例v2调用基类的test1函数时,test1调用的并不是基类定义的func1而是派生类的func1,虽然基类和派生类都有func2,但并不是虚函数,派生类的实例v2调用基类的test2函数时,test2调用的还是基类定义的func2。

如果能够好好理解这个函数调用的方式,我们就能很容易利用这一点。下面看看应用

class B
{
public:
    B( ){ }
    ~B( ){ }
   
    void Test( int v )
    {
        if ( v >= 0 ) OnTest1( );
        else OnTest2( );
    }

    virtual void OnTest1( ){ }
    virtual void OnTest2( ){ }
};

class BB
{
public
    virtual void OnTest1( ){ }
    virtual void OnTest2( ){ }
}

上面调用Test函数后,如果v大于等于0就调用OnTest1函数,如果小于0就调用OnTest2函数,这样我们在派生类中就只要在OnTest1和OnTest2函数中分别处理非负数和负数的情况,虚函数通常就用于事件的通知,有兴趣可以看看MFC的事件处理函数基本上都是虚函数。

虚函数还有一个应用就是限定析构函数,因为通常我们定义实例总是派生类,但是指针类型可能是基类的

比如:

B * p = new BB( );
if ( p ) delete p;

这时候delete p的时候只是调用了B的析构函数,如果我们把析构函数定义为虚函数,那么delete p的时候就会主动调用BB的析构函数,从而达到完全释放BB内存的目的。

 

纯虚类

纯虚类就是带有纯虚函数的基类,纯虚类不能被直接实例化

class A
{
    virtual int func( ) = 0;
};

A a; // 错误

纯虚类必须定义一个重载了纯虚函数的派生类才能使用

class AA
{
    virtual int func( ){ }
}

AA a; // 正确

实际上纯虚函数就是定义了一个函数指针,但是函数指针没有实例化,必须在派生类中实例化这个指针才能用。纯虚类有一个用处就是用来强制派生类实现某些特定的功能。

 

2、VC编译环境

这里只介绍VC++.NET 2003。编写DirectX程序,最重要的是将DirectX头文件和库文件包含进去,当然我们可以在#include的时候写上头文件的路径,使用#pragma来引用库文件

比如

#include "C:\\Program Files\\Microsoft DirectX 9.0 SDK (June 2005)\\Include\\d3d.h"

#pragma comment ( lib, "C:\\Program Files\\Microsoft DirectX 9.0 SDK (June 2005)\\Lib\\x64\\d3d9.lib" )

但这样显得有点长,包含文件多了还比较烦,SDK文件路径换了以后,还需要修改代码。

我们如果在VC的环境中添加这些路径,就不用写这么多了

选择菜单[工具]-[选项]弹出如下界面,选择[项目]-[VC++目录]选项



我们要设定的是[包含文件]和[库文件]两项,将DirectX的SDK目录下的include路径和lib路径添加进去即可,而且必须将这2个路径移动其他路径的前面。

Windows下面我们编写游戏客户端一般是建立Win32应用程序项目



在这个项目下,有几个选项



这样就建立了一个Windows程序了,当然我们也可以建立MFC应用程序框架和ATL应用程序框架,都可以,但是一般情况下,我们不去使用这两种框架,尤其是ATL框架,很少有游戏程序使用它。MFC是一个比较优秀的框架,有很多封装好的功能,如果大家对这个比较熟悉,我们也可以使用它。只是我们在发布我们的成果的时候不要忘了将MFC的DLL文件打包进安装程序就可以了。

我们来看看gc_img模块如何使用

建立好Windows程序以后,在stdafx.h中添加

#include "C:\\ngcore\\inc\\gc_img.h"

#pragma comment ( lib, "C:\\ngcore\\lib\\gc_img.lib" )

这样整个环境就搭好了,编译如果没有出现错误提示就可以进行开发了。

 

3、Win32应用程序

Win32应用程序都有一个入口WinMain函数,相当于C程序的入口main函数,WinMain函数要做3件事:注册WndClass,生成Window窗体,进行消息循环,如果觉得这里面的东西过于复杂,只要拷贝代码就好,VC编译器向导同时也会为我们生成这些代码,免去了我们来写这些代码。

下面是典型的WinMain函数

// 消息处理函数
LRESULT WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
    switch ( uMsg )
    {
    case WM_CLOSE: // 处理关闭窗口的事件,如果窗口关闭了就退出
        PostQuitMessage( 0 );
        break;
    }

    return DefWindowProc( hWnd, uMsg, wParam, lParam );
}

int APIENTRY WinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nCmdShow)
{
    MSG msg;
    memset( &msg, 0, sizeof(MSG) );

    WNDCLASS wc;
    if (!hPrevInstance)
    {
         wc.style = 0;
        wc.lpfnWndProc = (WNDPROC) WndProc; // 消息处理函数,这里就是一个函数指针
        wc.cbClsExtra = 0;
        wc.cbWndExtra = 0;
        wc.hInstance = hInstance;
        wc.hIcon = LoadIcon((HINSTANCE) NULL, IDI_APPLICATION);
        wc.hCursor = LoadCursor((HINSTANCE) NULL, IDC_ARROW);
        wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
        wc.lpszMenuName = "MainMenu";
        wc.lpszClassName = "MainWndClass"; //这个类名可以随便取

        if (!RegisterClass(&wc))
            return 0;
    }

    //这里计算窗口边框和标题栏的大小,使得窗口的客户区大小为800 * 600
    //如果我们直接在CreateWindow输入800 * 600,那么客户区的大小要比800 * 600小
    int nX = GetSystemMetrics( SM_CXFRAME );
    int nY = GetSystemMetrics( SM_CYFRAME ) + GetSystemMetrics( SM_CYCAPTION );


    //生成窗口
    HWND hwndMain = CreateWindow("MainWndClass", "Sample",
        WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
        CW_USEDEFAULT, CW_USEDEFAULT, 800 + nX, 600 + nY, (HWND) NULL,
        (HMENU) NULL, hInstance, (LPVOID) NULL);

    if (!hwndMain) return 0;

    ShowWindow(hwndMain, nCmdShow);
    UpdateWindow(hwndMain);


    // 处理消息循环
    while ( msg.message != WM_QUIT )
    {
        if ( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
        {
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
    }

    return (int)msg.wParam;
}

上面的代码可能和大家在某些地方看到的不一样,但基本的原理都是一样的,具体的函数的使用方法可以参考MSDN相关说明。

这样一个游戏窗口就建立好了,然后来添加一些代码建立gc_img程序

int APIENTRY WinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nCmdShow)
{
    MSG msg;
    memset( &msg, 0, sizeof(MSG) );

    WNDCLASS wc;
    if (!hPrevInstance)
    {
        wc.style = 0;
        wc.lpfnWndProc = (WNDPROC) WndProc; // 消息处理函数,这里就是一个函数指针
        wc.cbClsExtra = 0;
        wc.cbWndExtra = 0;
        wc.hInstance = hInstance;
        wc.hIcon = LoadIcon((HINSTANCE) NULL, IDI_APPLICATION);
        wc.hCursor = LoadCursor((HINSTANCE) NULL, IDC_ARROW);
        wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
        wc.lpszMenuName = "MainMenu";
        wc.lpszClassName = "MainWndClass"; //这个类名可以随便取

        if (!RegisterClass(&wc))
            return 0;
    }

    //这里计算窗口边框和标题栏的大小,使得窗口的客户区大小为800 * 600
    //如果我们直接在CreateWindow输入800 * 600,那么客户区的大小要比800 * 600小
    int nX = GetSystemMetrics( SM_CXFRAME );
    int nY = GetSystemMetrics( SM_CYFRAME ) + GetSystemMetrics( SM_CYCAPTION );


    //生成窗口
    HWND hwndMain = CreateWindow("MainWndClass", "Sample",
        WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
        CW_USEDEFAULT, CW_USEDEFAULT, 800 + nX, 600 + nY, (HWND) NULL,
        (HMENU) NULL, hInstance, (LPVOID) NULL);

    if (!hwndMain) return 0;

    // 这里进行系统初始化,返回一个根对象
    LPOBJ pRoot = gc_init( hInstance, hwndMain, 800, 600, true );

    ShowWindow(hwndMain, nCmdShow);
    UpdateWindow(hwndMain);


    // 处理消息循环
    while ( msg.message != WM_QUIT )
    {
        if ( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
        {
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
        else
        {
            gc_update( ARGB( 255, 0, 0, 0 ) );
        }
    }

    gc_destory( );

    return (int)msg.wParam;
}

添加上面的红色代码,就可以运行了,运行后可以看到一个黑色的背景和左上角的FPS信息


然后简单说说MFC框架下如何使用gc_img模块





同样在在stdafx.h中添加

#include "C:\\ngcore\\inc\\gc_img.h"

#pragma comment ( lib, "C:\\ngcore\\lib\\gc_img.lib" )

我们需要给App类(public CWinApp)重载3个函数InitInstance,ExitInstance,OnIdle,其中向导默认重载了InitInstance函数



然后在这3个函数中添加如下代码

BOOL Cmfc_testApp::InitInstance()
{
    InitCommonControls();

    CWinApp::InitInstance();

    // 初始化 OLE 库
    if (!AfxOleInit())
    {
        AfxMessageBox(IDP_OLE_INIT_FAILED);
        return FALSE;
    }
    AfxEnableControlContainer();
    SetRegistryKey(_T("应用程序向导生成的本地应用程序"));

    CMainFrame* pFrame = new CMainFrame;
    if (!pFrame)
        return FALSE;
    m_pMainWnd = pFrame;
    // 创建并加载带有其资源的框架
    pFrame->LoadFrame(IDR_MAINFRAME,
        WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE, NULL,
        NULL);
    // 唯一的一个窗口已初始化,因此显示它并对其进行更新
    pFrame->ShowWindow(SW_SHOW);
    pFrame->UpdateWindow();

    CRect rcClient;
    pFrame->GetClientRect( &rcClient );
    LPOBJ pRoot = gc_init( ::AfxGetInstanceHandle( ), pFrame->GetSafeHwnd( ),
        rcClient.Width( ), rcClient.Height( ), true );

    return TRUE;
}

BOOL Cmfc_testApp::OnIdle(LONG lCount)
{
    gc_update( ARGB( 255, 0, 0, 0 ) );
    return CWinApp::OnIdle(lCount);
}

int Cmfc_testApp::ExitInstance()
{
    gc_destory( );
    return CWinApp::ExitInstance();
}

效果一样

 

4、STL

模版是C++重要的一个变革,从另一个角度大大提升了C++代码可重用的方式。

template<typename _Tvn >
class A
{
    _Tvn md[10];
public:
    A( ){ }
    ~A( ) { }
};

当我们定义A的时候,这个类就分配了10个_Tvn变量,我们并不知道这10个是什么类型的变量,但当我们定义的时候,指定_Tvn类型,编译器就自动用我们指定的类型替换_Tvn,有点像宏,但不是简单的宏那么简单。

A<int> v1;

这样实际上上面的类A就变成了下面的样子

class A
{
    int md[10];
public:
    A( ){ }
    ~A( ) { }
};

如果我们定义A<float> v2;

就相当于又生成了一个类

class A
{
    float md[10];
public:
    A( ){ }
    ~A( ) { }
};

通常在游戏中,我们会定义很多很多的类型,而且经常需要不断的申请和释放这些类型空间,如果我们都使用数组或者每个都定义链表等比较麻烦,写起来也累,模版可以解决我们的问题。

VC自带有很多模版库,其中包含了STL(标准模版库),STL模版库的头文件也在VC的头文件目录下,而且都是开源,所有的源代码都有,有兴趣的可以自己看。STL的头文件基本上都没有扩展名。

比如,我们要使用STL的链表,那么

#include <list>
using namespace std; // 要加上这句,大部分STL库都在std命名空间里

list<int> v1; // 定义了一个整型的链表

实际上std::list是一个双向环形链表,看看源代码就知道了。

STL通过构造器来分配元素空间

比如

int * p = new int( 10 );
v1.push_back( (*p) ); //添加一个10的整型元素

STL是存储的元素的副本,也就是说v1存放的是新的实例,如果我们delete p,v1中还是存在一个10的整型元素。但是对于复杂的类型,比如我们自己定义的类,不能期望STL的构造器能够完全复制一个实例。

class A
{
};

list<A> v2;

A * p = new A( );
v2.push_back( (*p) );

这时候如果我们访问插入的这个元素,可能并不完全跟p一样,主要取决于A的成员构造情况。而且重新复制一个A的副本消耗的时间和空间都是比较可观的,因此,一般情况下并不存放实例到list中,而是存放指针进去。

class A
{
};

list<A*> v3;

A * p = new A( );
v3.push_back( p );

但是有一个问题,这时候,我们不能delete p,如果释放掉了,当我们再从list中取出这个指针的时候,这个指针就无效了。通常情况下,我们需要慎重的对list中的实例进行delete,将在后面的介绍中来详细描述这个问题。

STL元素的访问需要通过迭代器来处理

因为我们访问的每个元素并不一定有效,这样的STL就会显得很脆弱,而且访问元素效率下降很多。

list<A*>:perator it = v3.begin( );
for ( ; it != v3.end( ); ++it ) // 这里使用++it而不使用it++,凡是对重载操作符有理解的都应该明白
{
    A * p = (*it); // 这样就可以对p进行访问了
}

另外说一下删除元素,写过链表的都应该知道,如果删除了一个元素,当前的指针就无效了,那么我们就要重新取得迭代器

list<A*>::operator it = v3.begin( );
while( it != v3.end( ) )
{
   it = v3.erase( it ); // 删除一个元素
}

我们常用的还有

std::vector,向量表,有一点像动态数组,不过这个数组并不是动态的,每次分配一定大小的空间,如果当添加的元素超过了这个空间,就分配一个更大的,然后删除旧的。

std::map,红黑树(平衡树)

stdex::hash_map,哈希map,这个并不是STL中的模版,VC++.NET 2003中才有,VC6中没有这个模版,使用的时候,需要使用

using namespace stdex;

sf_200573174024.rar

696.42 KB, 下载次数:

89

主题

822

帖子

847

积分

高级会员

Rank: 4

积分
847
 楼主| 发表于 2005-7-3 17:42:00 | 显示全部楼层

Re: 周末写了点东西,大家帮忙看看

相关图片
sf_200573174210.jpg

24

主题

229

帖子

229

积分

中级会员

Rank: 3Rank: 3

积分
229
发表于 2005-7-4 10:08:00 | 显示全部楼层

Re:周末写了点东西,大家帮忙看看

好文,支持楼主!

2

主题

115

帖子

115

积分

注册会员

Rank: 2

积分
115
QQ
发表于 2005-7-5 10:52:00 | 显示全部楼层

Re:周末写了点东西,大家帮忙看看

顶上先

89

主题

822

帖子

847

积分

高级会员

Rank: 4

积分
847
 楼主| 发表于 2005-7-5 11:52:00 | 显示全部楼层

Re: 周末写了点东西,大家帮忙看看

关于文档,我单独打了一个包,关于代码,里面我发现了一些bug,以后再放上来

sf_200575115133.rar

298.43 KB, 下载次数:

89

主题

822

帖子

847

积分

高级会员

Rank: 4

积分
847
 楼主| 发表于 2005-7-5 11:52:00 | 显示全部楼层

Re: 周末写了点东西,大家帮忙看看

相关图片
sf_200575115211.jpg

1

主题

130

帖子

134

积分

注册会员

Rank: 2

积分
134
发表于 2005-7-17 21:30:00 | 显示全部楼层

Re:周末写了点东西,大家帮忙看看

好文章,顶

16

主题

404

帖子

404

积分

中级会员

Rank: 3Rank: 3

积分
404
发表于 2005-7-17 22:29:00 | 显示全部楼层

Re:周末写了点东西,大家帮忙看看

好文章!支持楼主!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-12-26 11:50

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表