SOM v3.3.3 二次开发中 C++ DLL用户自定义技能基础篇(一)
前言
根据大家的要求,已经上传用户自定义dll脚本代码。反正是白嫖,求求点个star啦:Github传送门
DLL,即动态链接库,是一种应用程序扩展。在SOM v3.3.3 二次开发中,这是一种用户自定义技能的脚本。
在之前的LUA脚本的教学中,我们已经充分感受到了官方函数的种种劣势 好像有点夸张 所以本期博客重点介绍我们的自定义用户技能。
这篇博客中我们不测试任何实例,仅仅使用一个实例作为模板讲解!
为了方便读者阅读,在这里先阐明本期的主题:我们本期重点讲解vector.h库以及我们的自定义技能脚本的框架。中间会渗透很多面向对象的教程。
再见了360
360陪伴我走过了漫长的岁月,我一直都是360的忠实支持者,今天(2020.4.9)我告别了它,开始使用火绒,写一段文字纪念一下。
我怎么就突然不用360了
我一直都很吹360,吹爆360
因为360真的好用。主要是简单,很简单,一切都很简单。
家里的无线密码不知道,但是想玩手机,刚好主机有网络,弄个网卡一插,360免费wifi可以把我的主机直接变成信号发射器。但是现在,我已经知道怎么通过路由器管理系统查询本地存在的可用wifi。
不懂系统重装和软件备份,360都有一键重装和一键备份,我不用百度,无需冒险尝试,就可以迅速用上崭新的系统。但是,如今我的要求是win10,360还停留在win7。我的要求是纯正,360有诸多为了初级用户而设定的捆绑软件……我也早已学会了备份……毕竟,3TB的BackupPlus不是白买的。
我想健康的使用电脑,防止天天撸代码用眼疲劳。360很贴心的每40分钟锁屏一次。但是如今的网课动辄一个半小时,四十分钟一次的提醒反而让人烦扰。
我想管理电脑的软件,我想有可靠的软件来源。可惜当时并不重视控制面板,也不知道运用官网或者sourceforge这样的知名网站。
随着自身计算机操作能力的不断增强,我渐渐不需要这些笨笨的功能,我开始反感360的主页勒索,反感360的杀毒报错,讨厌它不经过我的同意就胡乱杀我的软件,讨厌它复杂的信任区添加方式……
直到我最后使用360的原因只剩下当年的情怀时……
我还是把它卸载了。
还好我从没说过打死不用火绒之类的话
Old Example New Face
别问我为什么非要用英文
还是从最基本的脚本开始——抓球脚本
下面,我们会根据这个抓球脚本来逐步深入讲解一整套的C++ dll体系。
强烈建议已有面向对象的基础者学习,否则相当吃力!!!
我尽可能分析C++语法特性
先扔代码:
#include "Skill.h"
#include "utils/maths.h"
extern "C"_declspec(dllexport) PlayerTask player_plan(const WorldModel* model, int robot_id);
PlayerTask player_plan(const WorldModel* model, int robot_id) {
PlayerTask task;
const point2f playerPos = model->get_our_player_pos(robot_id);
const point2f ballPos = model->get_ball_pos();
const double playerDir = model->get_our_player_dir(robot_id);
double theta1 = abs(playerDir - (ballPos - playerPos).angle());
theta1 = theta1 < PI ? theta1 : 2 * PI - theta1;
if(theta1 < PI / 4 || (ballPos - playerPos).length() > ROBOT_HEAD + BALL_SIZE) task.target_pos = ballPos;
task.orientate = (ballPos - playerPos).angle();
return task;
}
C++ 二次开发基本框架(模板)简析
在看到复杂的抓球脚本时,内心不免涌过一丝紧张,然而,这些代码并不都要你自己身体力行地去撰写,官方已经给我们提供了一个良好的开发模板。
#include "utils/PlayerTask.h"
#include "utils/constants.h"
#include "utils/worldmodel.h"
#ifdef _DEBUG
#pragma comment (lib,"worldmodel_lib\\Debug\\worldmodel_lib.lib")
#else
#pragma comment (lib,"worldmodel_lib\\Release\\worldmodel_lib.lib")
#endif
extern "C" _declspec(dllexport) PlayerTask player_plan(const WorldModel * model, int robot_id);
PlayerTask player_plan(const WorldModel* model, int robot_id)
{
PlayerTask task;
return task;
}
实际上,预处理指令和接口定义我们都可以忽略,手写代码的时候,我们只需要关注PlayerTask函数即可。这里,我还是分析一下在PlayerTask函数前的部分内容。
编程前你需要知道的东西——头文件
官方的模板并没有引用所有的工具包,在这里,我们引用了三个头文件
#include "utils/PlayerTask.h"
#include "utils/constants.h"
#include "utils/worldmodel.h"
和刚刚的抓球脚本有所不同,我们在抓球脚本里引用的是
#include "Skill.h"
#include "utils/maths.h"
但是这两种引用基本是一致的,你可以查看Skill.h的定义,你会发现该文件下的代码包含了标准模板里面得到内容(请忽略constants.h):
#ifndef SKILL_H
#define SKILL_H
#include "utils/PlayerTask.h"
#include "utils/worldmodel.h"
class Skill {
Skill() {};
~Skill() {};
};
#endif
析构和构造
这里插播一个小话题,我们经常看见这种与类的名称完全一致的函数,那么它到底是什么呢???
为了初始化类,我们引入构造函数这一概念
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
除去宏处理和包含一些其他头文件外,这个头文件几乎没有什么意义。里面的skill类是一个空类,内部的构造函数为空,自然析构函数也是空。析构存在的目的是释放构造时所占用的空间。
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
说了这么多,skill类仍然是一个废类……
C++预处理器指令(宏)
我就是无聊讲一下,你也可以选择无视
回到模板代码,你会发现还有这些神奇代码:
#ifdef _DEBUG
#pragma comment (lib,"worldmodel_lib\\Debug\\worldmodel_lib.lib")
#else
#pragma comment (lib,"worldmodel_lib\\Release\\worldmodel_lib.lib")
#endif
这种上世纪的语言模式,就是C++中的预处理指令。
预处理指令负责指示编译器在实际编译之前所需完成的预处理。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。
比如,之前所有的实例中都有 #include 指令。这个宏用于把头文件包含到源文件中。
C++ 还支持很多预处理指令,比如 #include、#define、#if、#else、#line 等,我们这里就事论事,该说什么说什么。
条件编译
这里的**#ifdef-#endif语句用于选择性编译代码,正如上方代码一样,如果_DEBUG被#define**指令定义过,那么它将编译
#pragma comment (lib,"worldmodel_lib\\Debug\\worldmodel_lib.lib")
否则编译下方的
#pragma comment (lib,"worldmodel_lib\\Release\\worldmodel_lib.lib")
预编译的实例模拟
#include<iostream>
using namespace std;
int main(){
#define _NUM 666;
#ifdef _NUM
cout<<_NUM;
#else
cout<<1;
#endif
getchar();
return 0;
}
即兴写了一个样例。
上面的样例中,_NUM已经被宏替换,即定义,所以它将输出666,也就是**_NUM**的值。
C++程序接口
在上述代码中
extern "C" _declspec(dllexport) PlayerTask player_plan(const WorldModel * model, int robot_id);
提供了机器人的唯一借口的基本框架。extern用于标示变量或者函数的定义在别的文件中
_declspec这个我也不知道是什么玩意儿,从网络上截了一段:
“__declspec”是Microsoft c++中专用的关键字,它配合着一些属性可以对标准C++进行扩充。这些属性有:align、allocate、deprecated、 dllexport、dllimport、 naked、noinline、noreturn、nothrow、novtable、selectany、thread、property和uuid。
这一句虽然重要,但是我们在二次开发中根本无需理睬。
PlayerTask函数的形参表解析
PlayerTask player_plan(const WorldModel* model, int robot_id)
{
PlayerTask task;
return task;
}
上面给出了整个PlayerTask函数主体,这是一个以类作为返回值的函数,函数名为player_plan函数内部定义了一个PlayerTask型的类task,而且这个task也是作为函数的返回值返回去。
PlayerTask类在**“utils/PlayerTask.h”**库中被定义过,我们接下来看一看这里面都有什么:
以下是这个库中包含的PlayerTask类的定义源代码
//抽象基本任务;依据skill需要可以设置不同的任务;skill执行的任务继承PlayeTask;
class PlayerTask {
public:
PlayerTask() { memset(this, 0, sizeof(PlayerTask)); };
PlayerTask(const PlayerTask& task) { memcpy(this, &task, sizeof(PlayerTask)); }
~PlayerTask() {};
int flag;
RobotRole role; //球员角色
point2f target_pos; // 全局目标点
double orientate; // 全局目标到点朝向
point2f global_vel; // 全局目标到点平动速度
double rot_vel; // 全局目标到点转动速度
int rot_dir; // 旋转的方向
/// 运动参数 : 用于底层运动控制 ,指定标签辅助
double maxAcceleration; // 最大加速度
double maxDeceleration; // 最大减速度
/// 踢球参数 : 用于平射挑射控制 ,默认使用
bool needKick; // 踢球动作执行开关
bool isPass; // 是否进行传球
bool needCb;
bool isChipKick; // 挑球还是平射
double kickPrecision; // 踢球朝向精度
double kickPower; // 踢球力度
double chipKickPower; // 挑球力度
};
实际上这里所有附带注释的变量名称在二次开发手册中都有出现,他们便控制着机器人的task该如何执行,我们一般传回去的task对象中就包含着我们在里面修改的数据。
如果看到这里一脸懵逼没有关系,这里只是讲解官方函数的本体和来源,但是这些仍然和对二次开发的理解息息相关,如果真的看不懂,大可不必一直追究,可以先看后面的。
第二个整型变量无需多言robot_id记录的就是当前机器人的机号。
到此为止,二次开发的C++模板就讲完了。
回到我们的抓球脚本
#include "Skill.h"
#include "utils/maths.h"
extern "C"_declspec(dllexport) PlayerTask player_plan(const WorldModel* model, int robot_id);
PlayerTask player_plan(const WorldModel* model, int robot_id) {
PlayerTask task;
const point2f playerPos = model->get_our_player_pos(robot_id);
const point2f ballPos = model->get_ball_pos();
const double playerDir = model->get_our_player_dir(robot_id);
double theta1 = abs(playerDir - (ballPos - playerPos).angle());
theta1 = theta1 < PI ? theta1 : 2 * PI - theta1;
if(theta1 < PI / 4 || (ballPos - playerPos).length() > ROBOT_HEAD + BALL_SIZE) task.target_pos = ballPos;
task.orientate = (ballPos - playerPos).angle();
return task;
}
这一段代码和我一开始丢的是一样的,这里是为了读者方便不用跳上跳下。
这段代码的主要目的是朝向球抓球
在程序的最前面,我们要先获取我方机器人的坐标:
const point2f playerPos = model->get_our_player_pos(robot_id);
这个坐标是一个point2f类型的变量,point2f类型储存的是一个二维坐标系下的一个点,他被定义在vector.h下,我们不妨看一看它的定义,以更好地理解它。
//==== Vector types ====//
typedef Vector::vector2d<double> vector2d;
typedef vector2d point2d;
typedef Vector::vector2d_struct<double> vector2d_struct;
typedef Vector::vector2d<float> vector2f;
typedef vector2f point2f;
这里的最后一行代码定义了point2f型的变量。不难发现,这里的point2f本质上是vector2f变量。但是为什么?
杂谈typedef——变量类型重命名的瑞士军刀
我们且看下面这个例子:
#include<iostream>
using namespace std;
int main(){
typedef int inrr;
inrr a;
a=567;
cout<<a<<endl;
return 0;
}
上面这一段的代码输出的值是567,实际上,我将int类型的变量重命名为inrr,记住,这只是重命名。但是,我希望大家不要将这个和**#define视为一类,我们只能说在这个程序里,它起到的作用和#define相同,但是typedef**又不同于这个简单的文本替换。
typedef的实际作用是简化复杂的变量声明或者自定义变量名。
因此,point2f本质上就是vector2f,从vector2f的定义可以看出,它其实是vector2d
那么,我们只要研究vector2d是什么就可以了。
初识vector2d——二维坐标系下的平面向量变量
首先声明一点,这个和C++STL的vector不是一个东西
这是vector.h中对于vector2d类的描述
template <class num>
class vector2d {
public:
num x, y;
vector2d() {
x = 0;
y = 0;
}
vector2d(num nx, num ny) {
x = nx;
y = ny;
}
vector2d<num> get_vector2d() { return vector2d<num>(x, y); }
const num X() const { return x; }
const num Y() const { return y; }
void set_x(num nx) { x = nx; }
void set_y(num ny) { y = ny; }
void set(num nx, num ny) {
x = nx;
y = ny;
}
void set(const vector2d<num> p) {
x = p.x;
y = p.y;
}
vector2d<num>& operator=(vector2d<num> p) {
set(p);
return (*this);
}
vector2d<num>& operator=(vector2d_struct<num> p) {
set(p.x, p.y);
return (*this);
}
num length() const;
num sqlength() const;
//角度值是(-180,180】
num angle() const { return (atan2(y, x)); }
num dist(vector2d<num> t) const { return sqrt((x - t.x) * (x - t.x) + (y - t.y) * (y - t.y)); }
vector2d<num> norm() const;
vector2d<num> norm(num len) const;
void normalize();
vector2d<num> bound(num max_length) const;
num dot(vector2d<num> p) const;
num cross(vector2d<num> p) const;
vector2d<num> rotate(num theta) {
num dir = angle() + theta;
num x = length() * cos(dir);
num y = length() * sin(dir);
return vector2d<num>(x, y);
}
vector2d<num> perp() { return (vector2d(-y, x)); }
friend std::ostream& operator <<(std::ostream& out, const vector2d<num>& t) {
out << t.x << "\t" << t.y << "\t";
return out;
}
vector2d<num>& operator+=(vector2d<num> p);
vector2d<num>& operator-=(vector2d<num> p);
vector2d<num>& operator*=(vector2d<num> p);
vector2d<num>& operator/=(vector2d<num> p);
vector2d<num> operator+(vector2d<num> p) const;
vector2d<num> operator-(vector2d<num> p) const;
vector2d<num> operator*(vector2d<num> p) const;
vector2d<num> operator/(vector2d<num> p) const;
vector2d<num> operator*(num f) const;
vector2d<num> operator/(num f) const;
vector2d<num>& operator*=(num f);
vector2d<num>& operator/=(num f);
vector2d<num> operator-() const;
bool operator==(vector2d<num> p) const;
bool operator!=(vector2d<num> p) const;
bool operator<(vector2d<num> p) const;
bool operator>(vector2d<num> p) const;
bool operator<=(vector2d<num> p) const;
bool operator>=(vector2d<num> p) const;
vector2d<num> rotate2same_coord_standard(double a) const;
vector2d<num> rotate2opp_coord_standard(double a) const;
vector2d<num> perp() const;
};
在这里,所有的成员都是默认公开,你可以自由调用。上述代码并没有给完整的代码,你可以发现,这个类中基本所有成员函数,都只有声明。我在之后会提供这些声明的注释并告诉大家用法
对vector2d类的简单分析
不难发现,这是一个模板类
因此,我们可以看见熟悉的标识符
template <class num>
以及下面的一大堆可怕的定义。
我们不妨从构造函数开始,逐步地剖析这个类里面都有什么。
num x, y;
vector2d() {
x = 0;
y = 0;
}
变量x,y都是公开属性,在构造函数中被设置为0,但是这个类中不是一个构造函数
vector2d(num nx, num ny) {
x = nx;
y = ny;
}
往往形参列表不同的函数可以再同一层次共存,这一现象被称为函数重载,实际上构造函数也是可以重载的。
如果有这样的重载声明,那我们也可以不使用默认构造,我们可以在定义类的时候利用圆括号传参,构造我们想要的vector。也正是存在这样的重载构造函数,以下get_vector2d函数的返回才存在可能:
vector2d<num> get_vector2d() { return vector2d<num>(x, y); }
我们继续往下看,下面几个成员函数都是一些基础的设定函数:
const num X() const { return x; }
const num Y() const { return y; }
void set_x(num nx) { x = nx; }
void set_y(num ny) { y = ny; }
void set(num nx, num ny) {
x = nx;
y = ny;
}
一二两行的函数保证了在返回x,y的时候其值不会被恶意或无意改动,提升了安全性,这个和直接返回x,y是有区别的。注意const修饰符
下面三个无返回类型的函数便是修改两个成员x,y的函数了。他们组合在一起之后,既支持单个修改,也支持一同修改。
另外,vector本身附带有许多重载
void set(const vector2d<num> p) {
x = p.x;
y = p.y;
}
vector2d<num>& operator=(vector2d<num> p) {
set(p);
return (*this);
}
特地把set函数截到这里
注意,这里的set和刚刚给大家的set是完全不一样的!!!
这里的重载函数是重载等于号,通过调用set函数完成两个向量的赋值,其中这里采用了函数返回值引用,并且返回了一个this指针,但是,他返回的不是一个地址,而是一个全新的类。
你可以直接使用等号复制到一个全新的类中,而且这个等于号无需重载,因为它类似于memcpy,是直接复制字节信息的。
举个栗子:
#include<iostream>
#include<algorithm>
using namespace std;
class father{
private:
int var=0;
public:
father(int a=1,int b=2,int c=3){
x=a;y=b;z=c;
return;
}
int x,y,z;
father& set(int num){var=num;return *this;}
void print(){cout<< var <<endl;}
// father* adreess(){return this;}
};
class son:public father{
public:
son(int a,int b,int c):father(a,b,c){}
};
int main(){
father test(1,2,3);
auto tes=test.set(5);
tes.print();
// father* adr=test.adreess();
// for(father* i=adr;i<=adr+10;i++);
getchar();
return 0;
}
请无视我的注释,因为我本来不想举这个例子,我只是无意间看到我以前做过这个文件,就顺便拿出来展示了一下。程序测试没有问题
你会发现我顺利地使用了这句话,没有报错
auto tes=test.set(5);
而且之后对tes的调用很正常。输出结果为5
到此为止,vector算是基本讲完了,后面都是一些函数声明,我们在具体使用的时候再具体分析!