SOM v3.3.3 C++ DLL用户自定义技能实战

SOM v3.3.3 二次开发中 C++ DLL用户自定义技能基础篇(一)

前言

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算是基本讲完了,后面都是一些函数声明,我们在具体使用的时候再具体分析!

到此为止,我们讲完了Vector,大家不要忽视了Vector的重要性,它基本上是我们后期编程的基本概念。下一期我们把目光转向WorldModel工具包

  • 24
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值