GDExtension C++环境搭建与使用
目录
介绍
在 Godot 中我们可以除了可以使用GDScript或C#来编写代码,还可以使用官方提供的GDExtension来使用C++来编写代码,然后将其编译为库文件在Godot中使用。
为什么使用GDExtension?
- 可以使用cpp的库,并把它集成到Godot中
- 如果希望提高性能,可以自己编写游戏逻辑代码
- 可以使用cpp集成其他语言,例如Lua
- 。。。
GDExtension官方文档:https://docs.godotengine.org/en/stable/tutorials/scripting/gdextension/gdextension_cpp_example.html
环境搭建预备工作
搭建 GDExtension C++ 环境我们需要下载:
-
Godot引擎(4.0版本)
Godot官网:https://godotengine.org/ 或在Steam上下载
注:GDExtension 不支持Godot4以下的版本,因此要使用GDExtension需要下载4.0版本. -
C++编译器
既然选择了使用 C++,相信大家都对 C++ 有一定了解,这方面因该不成问题,因此不再赘述。 -
SCons
SCons是一个由Python编写的自动化构建工具,我们将使用Python语法来编写一个SConstruct文件用于构建我们的项目。所以在下载构建工具SCons 之前我们还需要先下载 python
Python官网:https://www.python.org/downloads/
下载完成之后可以win+r cmd 输入python如果显示版本信息则说明安装成功
如果显示“python不是内部或外部命令……"将python添加进环境变量即可。
python安装成功之后通过"pip install scons"指令来下载scons
构建工具的话大家可能对scons也许不是太熟悉,我也是因为GDExtension才了解到,平时都是在使用CMake。熟悉它的可以继续下面的操作。如果对其不是很了解的话可以看一看附录中的SCons使用指南,对接下来编写自己的SConstruct文件有一定的帮助。但也可以先继续下面的操作最后再去看,用到SConstruct直接复制即可。
-
godot-cpp
在github上下载对应版本的godot-cpp:godot-cpp。
4.1的下载4.1的分支。
注:4.1 的GDExtension 相较于 4.0 发生了一些变化,虽然不大,但是4.1还是不兼容4.0。如果看官方的文档的话需要注意看与自己的godot对应的版本
完成以上工作之后我们可以正式开始搭建 GDExtension C++环境了。
正式开始搭建GDExtension C++环境
-
创建一个文件夹,名称随意如TestExtension。
-
将从github上下载的godot-cpp复制到该文件夹中。
-
在TestExtension中创建文件夹src(C++代码将在此文件夹中编写,如果复制接下来的SConstruct文件的话,目录结构要和我保持一致,否则编译时可能找不到所编写的C++代码。),接下来把 godot-cpp/test/src/ 中的register_types.h与register_types.cpp复制到该文件夹中。这个test是官方写的一个例子,可以看看学习一下,也可以直接复制这个test里的SConstruct。
注:如果使用4.1版本的分支,需要添加env.Append(CXXFLAGS=‘/source-charset:utf-8’),详细的可以看看我的另一篇文章 -
创建一个名为 SConstruct 的文件,并在该文件中写入以下代码.
import os import sys # 在godot-cpp下也有一个Sconstruct文件,需要读取它 env = SConscript("godot-cpp/SConstruct") # CPPPATH指定头文件路径 env.Append(CPPPATH = "src/") # 将编译src下的所有.cpp文件 src = Glob("src/*.cpp") if env['platform'] == 'linux': pass elif env['platform'] == 'windows': libpath = "libtest{}{}".format(env["suffix"], env["SHLIBSUFFIX"]) sharedlib = env.SharedLibrary( # 会创建一个lib文件夹,并在里面生成库文件 "lib/test{}{}".format(env["suffix"], env["SHLIBSUFFIX"]), src ) Default(sharedlib) elif env['platform'] == 'android': pass
这个时候我们的目录结构应该是这样的
随后我们就可以在src目录下编写自己的cpp代码了
test.h#pragma once #include <godot_cpp/classes/sprite2d.hpp> using namespace godot; class Test : public Sprite2D { GDCLASS(Test, Sprite2D ) protected: static void _bind_methods(); public: Test(); ~Test(); };
test.cpp
#include "test.h" #include <godot_cpp/variant/utility_functions.hpp> void Test::_bind_methods() {} Test::Test() { UtilityFunctions::print("hello world"); } Test::~Test() { UtilityFunctions::print("goodbye world"); }
虽然代码已经写完了但是Godot并不知道它的存在,接下来就是要把我们自己写的类注册到Godot中,还记得刚才复制进src的两个文件吗,现在是用到它们的时候了。
首先来到 register_types.h 文件中#ifndef EXAMPLE_REGISTER_TYPES_H #define EXAMPLE_REGISTER_TYPES_H #include <godot_cpp/core/class_db.hpp> using namespace godot; // 这两函数分别在加载和卸载的时候调用,我们可以更改它的名字。这里把example改为了test void initialize_test_module(ModuleInitializationLevel p_level); void uninitialize_test_module(ModuleInitializationLevel p_level); #endif
之后进入register_types.cpp文件中,把我们刚才编写的类注册进Godot
#include "register_types.h" #include <gdextension_interface.h> #include <godot_cpp/core/class_db.hpp> #include <godot_cpp/core/defs.hpp> #include <godot_cpp/godot.hpp> // 别忘了头文件 #include "test.h" using namespace godot; void initialize_test_module(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } // 将我们自己编写的类注册进Godot ClassDB::register_class<Test>(); } void uninitialize_test_module(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } } extern "C" { // Initialization. // 这里是编译为库文件之后暴露给Godot的接口,该函数的函数名可以更改,但要记住,之后会用到 GDExtensionBool GDE_EXPORT test_library_init(const GDExtensionInterface *p_interface, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { godot::GDExtensionBinding::InitObject init_obj(p_interface, p_library, r_initialization); init_obj.register_initializer(initialize_test_module); init_obj.register_terminator(uninitialize_test_module); init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); return init_obj.init(); } }
完成以上所有步骤之后,我们就可以开始编译了。在我们自己编写的SConstruct文件所在的目录中进入cmd,也就是在TestGDExtension下使用scons命令进行编译。首次编译所花费的时间会比较长,之后再编译的话会比较快。
编译完成后会生成几个库文件,根据我们所编写的SConstruct文件,会生成一个lib文件夹并在该文件夹中生成。复制lib下的.dll文件,接下来要配置Godot项目使用该动态库。
创建一个Godot项目,在项目中创建一个文件夹,将刚才的.dll动态链接库文件复制到该文件夹下,并在该文件夹下创建一个以.gdextension为后缀的文件,写入以下内容。[configuration] # 之前让记住的那个函数的函数名,可在register_types.cpp中查看复制过来 entry_symbol = "test_library_init" [libraries] # 与生成的动态库文件名保持一致(动态库的路径,因为该文件与动态库文件在同一个文件夹中 # 所以直接写动态库的名称) windows.debug.x86_64 = "test.windows.template_debug.x86_64.dll"
注:如果是4.1版本需要添加 compatibility_minimum = 4.1,如下:
[configuration] entry_symbol = "test_library_init" compatibility_minimum = 4.1 [libraries] windows.debug.x86_64 = "库路径"
如果以上步骤完成的没有错误的话,我们这时候创建节点会在Sprite2D节点下找到我们自定义的节点。
自此我们已经完成了环境的搭建,并且编写了一个简单的类注册到了Godot之中。接下来将介绍GDExtension C++的使用。
GDExtension C++的常用的方法
经过以上步骤相信大家都已经搭建好了GDExtension cpp的环境,接下来将介绍一些比较常用的方法。
1.添加方法
我们可以使用bind_method()来向Godot中添加方法
static godot::MethodBind
*godot::ClassDB::bind_method<godot::MethodDefinition, void (Test::*)()>(
godot::MethodDefinition p_method_name,
void (Test::*p_method)()
)
现在我们来向Test类中添加一个sayHello()方法(以下为在刚才的基础上新添加的代码)
test.h
...
public:
void say_hello();
...
test.cpp
#include <windows.h>
#pragma comment(lib,"User32.lib")
void Test::_bind_methods() {
ClassDB::bind_method(D_METHOD("sayHello"), &Test::say_hello);
}
void Test::say_hello() {
MessageBoxW(nullptr, L"hello", L"infor", MB_OK);
}
在重新使用scons命令编译一下,然后将新生成的动态库复制到Godot中,随后为Test节点挂载脚本来测试以下sayHello()方法。
运行一下看看效果
可以看到成功的弹出了一个windows的消息框
尝试在一个方法内调用另一个方法。
void sayWow() {
UtilityFunctions::print("wow");
}
void Test::callWow() {
sayWow();
}
void Test::_bind_methods() {
ClassDB::bind_method(D_METHOD("callWow"), &Test::callWow);
}
可以看到调用callWow()方法,成功的输出了wow
2.void _process(double delta);
来使用一下_process(double delta)这个函数,相信大家对这个函数并不陌生。继续添加新的代码
test.h
public:
void _process(double delta);
private:
double time_passed;
test.cpp
void Test::_process(double delta) {
time_passed += delta;
// 使节点做圆周运动
Vector2 new_position = Vector2(
10 * cos(time_passed * 10),
10 * sin(time_passed * 10)
);
set_position(new_position);
}
去godot中看看效果
如果运行时节点静止,先把附加在节点上的脚本给卸载掉
3.添加属性
接下来向引擎中添加两个属性分别来控制圆周运动的半径和速度
使用下述方法来添加
static void
godot::ClassDB::add_property(
const godot::StringName &p_class, // 向哪个类添加属性
const godot::PropertyInfo &p_pinfo, // 属性的信息
const godot::StringName &p_setter, // set方法
const godot::StringName &p_getter, // get方法
int p_index = -1
)
test.h
public:
// set/get方法
void set_radius(double radius);
double get_radius() const;
void set_speed(double speed);
double get_speed() const;
private:
double radius; // 控制做圆周运动的半径
double speed; // 控制做圆周运动的速度
test.cpp
void Test::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_radius"), &Test::get_radius);
ClassDB::bind_method(D_METHOD("set_radius", "radius"), &Test::set_radius);
ClassDB::bind_method(D_METHOD("get_speed"), &Test::get_speed);
ClassDB::bind_method(D_METHOD("set_speed", "speed"), &Test::set_speed);
ClassDB::add_property("Test",
PropertyInfo(Variant::FLOAT, "radius"),
"set_radius",
"get_radius"
);
ClassDB::add_property("Test",
PropertyInfo(Variant::FLOAT, "speed"),
"set_speed",
"get_speed"
);
}
Test::Test() {
// 在构造函数中给初始值
radius = 10.0;
speed = 5.0;
UtilityFunctions::print("hello world");
}
// 不要忘记修改原来写死速度和半径
void Test::_process(double delta) {
time_passed += delta;
Vector2 new_position = Vector2(
radius * cos(time_passed * speed),
radius * sin(time_passed * speed)
);
set_position(new_position);
}
void Test::set_radius(double radius) {
this->radius = radius;
}
double Test::get_radius() const {
return radius;
}
void Test::set_speed(double speed) {
this->speed = speed;
}
double Test::get_speed() const {
return speed;
}
看看效果
4.给半径和速度限定一个范围
使用 PROPERTY_HINT_RANGE 说明限定范围,后面的字符串用于说明"最小值,最大值,步长"
test.cpp
ClassDB::add_property("Test",
PropertyInfo(Variant::FLOAT, "radius", PROPERTY_HINT_RANGE, "0,500,10"),
"set_radius",
"get_radius"
);
ClassDB::add_property("Test",
PropertyInfo(Variant::FLOAT, "speed", PROPERTY_HINT_RANGE, "0,30,0.1"),
"set_speed",
"get_speed"
);
看看效果
5.为属性添加分组
test.h
public:
void set_hp(int hp);
int get_hp() const;
private:
int hp;
test.cpp
ADD_GROUP("Group_1", "first_");
ADD_SUBGROUP("Group_2", "first_second_");
ClassDB::bind_method(D_METHOD("set_hp", "hp"), &Test::set_hp);
ClassDB::bind_method(D_METHOD("get_hp"), &Test::get_hp);
ADD_PROPERTY(
PropertyInfo(Variant::INT, "first_second_Health Point"),
"set_hp",
"get_hp"
);
效果如图所示
6.常量
-
枚举
test.hclass Test { public: enum testEnum { MON, TUE, WED = 7, }; } VARIANT_ENUM_CAST(Test::testEnum);
注意:需要使用VARIANT_ENUM_CAST转换一下
test.cpp
BIND_ENUM_CONSTANT(MON); BIND_ENUM_CONSTANT(TUE); BIND_ENUM_CONSTANT(WED);
BIND_ENUM_CONSTANT()为定义的一个宏
#define BIND_ENUM_CONSTANT(m_constant) godot::ClassDB::bind_integer_constant(get_class_static(), godot::__constant_get_enum_name(m_constant, #m_constant), #m_constant, m_constant); Expands to: godot::ClassDB::bind_integer_constant(get_class_static(), godot::__constant_get_enum_name(MON, "MON"), "MON", MON);
-
flag
test.henum testFlag { Run, Idle }; VARIANT_BITFIELD_CAST(Test::testFlag);
也不要忘了使用VARIANT_BITFIELD_CAST转换一下
test.cpp
BIND_BITFIELD_FLAG(Run); BIND_BITFIELD_FLAG(Idle);
同理
#define BIND_BITFIELD_FLAG(m_constant) godot::ClassDB::bind_integer_constant(get_class_static(), godot::__constant_get_bitfield_name(m_constant, #m_constant), #m_constant, m_constant, true); Expands to: godot::ClassDB::bind_integer_constant(get_class_static(), godot::__constant_get_bitfield_name(Run, "Run"), "Run", Run, true);
-
constant
test.henum { MAX_HEALTH = 100 };
注意:常量不需要像上面的 enum 和 flag 一样需要转换
test.cpp
BIND_CONSTANT(MAX_HEALTH);
随便提一下,我们可以通过点击属性面板上的 doc 来调出文档查看节点的信息
来看看效果
7.信号
接下来我们将使用添加一个信号,当节点位置发生改变的时候发出这个信号,并且携带两个信息:发送该信号的节点和当前节点的位置
test.cpp
void Test::_bind_methods() {
...
ADD_SIGNAL(
MethodInfo(
"position_changed", // 信号的名称
PropertyInfo(Variant::OBJECT, "node"), // 参数1
PropertyInfo(Variant::VECTOR2, "new_position") // 参数2
)
);
...
}
void Test::_process(double delta) {
time_passed += delta;
Vector2 new_position = Vector2(
radius * cos(time_passed * speed),
radius * sin(time_passed * speed)
);
Vector2 current_position = get_position();
set_position(new_position);
if (current_position.x != new_position.x
|| current_position.y != new_position.y) {
// 发送信号
emit_signal("position_changed", this, new_position);
}
}
GDExtension 的一些常用的使用就先介绍到这里,更多的可以试着自己发掘。
附录
SCons 使用指南
Program
Program():用于生成可执行文件
当前目录结构
test
|-test.cpp
|-Sconstruct
#include <iostream>
int main() {
std::puts("hello world");
}
# 没有指定生成可执行文件的名称,按照test.cpp的名称生成test.exe
Program("test.cpp")
进入Sconstruct文件的目录中使用scons命令
PS F:\C++\test\testSconsInCLion> cd test
PS F:\C++\test\testSconsInCLion\test> scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fotest.obj /c test.cpp /TP /nologo
test.cpp
link /nologo /OUT:test.exe test.obj
scons: done building targets.
PS F:\C++\test\testSconsInCLion\test>
执行完毕后,可以看到生成了很多文件,
├─test
│ .sconsign.dblite
│ Sconstruct
│ test.cpp
│ test.exe
│ test.obj
执行一下test.ext看看,可以看到成功的输出了hello world
PS F:\C++\test\testSconsInCLion\test> ./test
hello world
使用 scons -c 清除命令
PS F:\C++\test\testSconsInCLion\test> scons -c
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed test.obj
Removed test.exe
scons: done cleaning targets.
执行完毕后,test.obj和test.exe被删除
├─test
│ .sconsign.dblite
│ Sconstruct
│ test.cpp
│
scons -Q:不生成多余的信息
PS F:\C++\test\testSconsInCLion\test> scons -Q
cl /Fotest.obj /c test.cpp /TP /nologo
test.cpp
link /nologo /OUT:test.exe test.obj
现在来指定一下生成的可执行文件的名称
Program("hello", "test.cpp")
.sconsign.dblite
hello.exe
Sconstruct
test.cpp
test.obj
多个源文件的情况,使用列表来存放源文件
├─test
│ .sconsign.dblite
│ sayHi.cpp
│ sayHi.h
│ Sconstruct
│ test.cpp
如果没有指定生成的可执行文件的名称将会以列表中的第一个源文件的名称作为可执行文件的名称,这里显示指定为hello
Program("hello", ["test.cpp", "sayHi.cpp"])
或是这样
src = ["test.cpp", "sayHi.cpp"]
Program("hello", src)
PS F:\C++\test\testSconsInCLion\test> scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fotest.obj /c test.cpp /TP /nologo
test.cpp
cl /FosayHi.obj /c sayHi.cpp /TP /nologo
sayHi.cpp
link /nologo /OUT:hello.exe test.obj sayHi.obj
scons: done building targets.
可以看到生成的所有文件都和我们的代码混在一起,很难看。之后将指定生成的文件的路径让它们和代码分开
.sconsign.dblite
hello.exe
sayHi.cpp
sayHi.h
sayHi.obj
Sconstruct
test.cpp
test.obj
运行一下,也没有问题
PS F:\C++\test\testSconsInCLion\test> ./hello
Hi
hello world
Object
Object() 用于生成目标文件(.obj文件)
Object("hello.cpp")
PS F:\C++\test\testSconsInCLion\testObj> scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fohello.obj /c hello.cpp /TP /nologo
hello.cpp
scons: done building targets.
可以看到只有obj文件,没有了.exe文件,
.sconsign.dblite
hello.cpp
hello.obj
SConstruct
Program不止可以编译源文件
Object("hello.cpp")
Program("helloWorld", "hello.obj")
scons: Building targets ...
cl /Fohello.obj /c hello.cpp /TP /nologo
hello.cpp
link /nologo /OUT:helloWorld.exe hello.obj
scons: done building targets.
PS F:\C++\test\testSconsInCLion\testObj> ./helloWorld
hello world
.sconsign.dblite
hello.cpp
hello.obj
helloWorld.exe
SConstruct
指定生成路径
当前的目录结构
├─testObj
│ .sconsign.dblite
│ hello.cpp
│ SConstruct
# 将在build/obj下生成hello.obj
Object("build/obj/hello.obj", "hello.cpp")
# 在 build 下生成helloWorld.exe
Program("build/helloWorld", "build/obj/hello.obj")
PS F:\C++\test\testSconsInCLion\testObj> scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fobuild\obj\hello.obj /c hello.cpp /TP /nologo
hello.cpp
link /nologo /OUT:build\helloWorld.exe build\obj\hello.obj
scons: done building targets.
现在的相比之前就清晰很多
├─testObj
│ │ .sconsign.dblite
│ │ hello.cpp
│ │ SConstruct
│ │
│ └─build
│ │ helloWorld.exe
│ │
│ └─obj
│ hello.obj
稍微改造一下
│ .sconsign.dblite
│ main.cpp
│ SConstruct
env = Environment()
# 设置生成文件的路径
obj_dir = 'build/obj'
target = 'build'
env.Object(target = obj_dir + '/main', source = 'main.cpp')
env.Program(target = target + '/main', source = obj_dir + '/main')
编译之后
│ .sconsign.dblite
│ main.cpp
│ SConstruct
│
└─build
│ main.exe
│
└─obj
main.obj
CPPPATH
CPPPATH用于指定头文件路径
│ .sconsign.dblite
│ SConstruct
│
├─header
│ main.h
│
└─src
main.cpp
#include "main.h"
int main() {
sayHello();
}
env = Environment()
env.Program('hello', "src/main.cpp")
此时如果编译就会报错无法打开"main.h"文件,因为它在header下所以找不到它。
···
PS F:\C++\test\testSconsInCLion\testGlob> scons
scons: Reading SConscript files …
scons: done reading SConscript files.
scons: Building targets …
cl /Fosrc\main.obj /c src\main.cpp /TP /nologo
main.cpp
src\main.cpp(1): fatal error C1083: 无法打开包括文件: “main.h”: No such file or directory
scons: *** [src\main.obj] Error 2
scons: building terminated because of errors.
···
这次我们来向构造环境中加入指定的头文件路径
env = Environment()
env.Append(CPPPATH = "header/")
env.Program('hello', "src/main.cpp")
可以看到这次编译成功了
PS F:\C++\test\testSconsInCLion\testGlob> scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
cl /Fosrc\main.obj /c src\main.cpp /TP /nologo /Iheader
main.cpp
H:\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include\ostream(743): warning C4530: 使用了 C++ 异常处理程序,但未启用展开语义
。请指定 /EHsc
header\main.h(7): note: 查看对正在编译的函数 模板 实例化“std::basic_ostream<char,std::char_traits<char>> &std::operator <<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,const char *)”的引用
link /nologo /OUT:hello.exe src\main.obj
scons: done building targets.
Glob
前面提到过如果有多个源文件我们可以把它们放在一个python列表中,但如果源文件很多一个一个写的话不够优雅很麻烦。这时我们可以使用Glob()函数,我们可以给它匹配条件让它来返回满足条件的编译对象。
.sconsign.dblite
sayHi.cpp
sayHi.h
Sconstruct
test.cpp
我们使用一个通配符来匹配所有的.cpp文件
src = Glob("*.cpp")
Program("hello", src)
那如果除了.cpp文件之外还有.c文件呢
.sconsign.dblite
sayHello.c
sayHello.h
sayHi.cpp
sayHi.h
Sconstruct
test.cpp
可以像这样写
src = Glob("*.cpp")
src = src + Glob("*.c")
Program("hello", src)
使用 extern “C” 告诉编译器这部分代码使用C的格式进行编译
#include "sayHi.h"
extern "C" {
#include "sayHello.h"
}
#include <iostream>
int main() {
sayHi();
sayHello();
std::puts("hello world");
}
SConscript
读取一个或多个sconscript脚本,返回一个node列表,node是指一个编译对象
│ SConstruct
│
└─src
main.cpp
SConstruct
src下的SConstruct
Program('main', 'main.cpp')
外部的SConstruct
SConscript("src/SConstruct")
当我们在src外使用scons命令时,外部的SConstruct就会去读取src下的那个Sconstruct文件进行编译
编译库文件
SharedLibrary() 用来编译动态库
│ .sconsign.dblite
│ SConstruct
│
├─header
│ hello.h
│
└─src
hello.c
SharedLibrary('build/lib/sayHello', 'src/hello.c')
编译之后
│ .sconsign.dblite
│ SConstruct
│
├─build
│ └─lib
│ sayHello.dll
│
├─header
│ hello.h
│
└─src
hello.c
hello.obj
SharedLibrary() 还可以用来编译目标文件,对上面的稍加改造
Object("build/obj/hello", "src/hello.c")
SharedLibrary('build/lib/sayHello', 'build/obj/hello.obj')
编译之后
│ .sconsign.dblite
│ SConstruct
│
├─build
│ ├─lib
│ │ sayHello.dll
│ │
│ └─obj
│ hello.obj
│
├─header
│ hello.h
│
└─src
hello.c
还可以源文件与目标文件混在一起,这里就不尝试了,感兴趣的可以自己试一试
StaticLibrary() 用于编译静态库
具体使用方法和动态库的一致,也就不再演示了
scons官方文档
SCons的一些基本的操作应该介绍的差不多了,限于文章篇幅更多的需要读者自己去搜索学习了。