当前位置:   article > 正文

Unity3D学习笔记#8_虚拟空战系统开发_flightgear 空战

flightgear 空战

学习U3D的主要目的是希望为实验室和自己开发一个虚拟空战的视景系统,经过一个月左右的学习和开发,目前已经完成了初步的版本(完成后又放飞自我了一段时间,自我批评中…),那么这篇主要介绍一下开发的这个系统,记录下思路和实现的基本过程。

本文仅供参考,工程不打算开源。

1. 开发目标

首先分析下需求。

空战演示有两种类型:一类是从飞机驾驶员的角度进行的,也就是我们从画面上看到的应该是驾驶舱中的重要仪表(多功能显示器、HUD等)和舱外的画面。这类研究中,我们只关注单个飞机上功能的设计和实现。比如,我们可以研究更好的飞机数据显示方案、研究辅助驾驶系统等等。另一类则是从上帝视角进行的,这也是现在比较热点的研究:协同和集群。主要目的是演示多机之间的配合,研究多机协同和决策算法等。

所以,我们希望开发的系统要支持驾驶舱内部视角,支持仪表的开发;也要支持上帝视角,从全局观察多个飞机的情况。

为了让软件功能更丰富,我们希望能够支持多种飞机类型,支持编辑建筑物和山地,支持显示路径规划的结果,支持在屏幕上绘制仪表,支持导弹、机炮射击、爆炸等效果。

总结起来,可以概括为:

  • 单机的研究。单机(弹)的控制率研究,无人机决策算法研究,民航飞机飞管系统研究,战斗机机动决策研究等等;
  • 多机的研究。多机协同研究,集群(蜂群)作战研究,民机调度研究等;
  • 路径规划。单机、多机路径规划算法研究。

这些内容通过U3D都是可以实现的。很多细节也是边开发边想清楚的,上面的讨论只是总体的方向。实际上,开发一个系统,很多时候最初并没有特别清晰的设计,只是有大概的想法和目标,细节方面很多都需要在实践中完善。

个人能力有限,不追求画面多么精致,能够基本支持预期的功能即可。系统的总体结构参考FlightGear软件,也就是基于通信的方式。

2. 总体框架

在这里插入图片描述
仿照FlightGear,让应用程序和视景演示程序分离,中间通过UDP进行连接,这有很多好处:首先软件耦合度低,视景部分完全独立,作为一个exe程序存在,可以放到任何机器上运行,研究的部分则可以使用任何语言开发(目前只提供了C++的接口,可以仿照C++接口实现其它语言的接口,或者包装C++接口),这样可以根据实际需要采用合适的语言,比如研究AI可能就使用Python了;其次,可以实现分布式结构,比如我们研究多机之间对抗,每个飞机在不同的计算机上运行,那么在局域网内,我们用一台机器作为视景环境,其它机器都把数据发送到这个演示的机器上就可以了。

视景软件的使用者只需要了解UDP消息的定义即可,其余都不必关心。因此这样的架构适用性是比较广的。此外,在第一小节中也进行了介绍,就是视景软件支持定制化显示功能,可以通过通信绘制UI界面,从而实现场景中数据的显示,个性化仪表的绘制等功能,可拓展性比较好。

3. 通信协议

由于不需要进行无线通信,也只在局域网下工作,因此不需要特别考虑通信效率的问题,那么为了便于理解和开发,直接采用了JSON的数据结构,其本质是一个字符串。

数据格式为:

{“msg_id”:…, “msg_type”:…, “msg_body”:{[…:…, ]…}}

其中,…表示省略的内容,根据实际数据填充,[]表示可选项,[]…表示可以重复多次的可选项,大括号和冒号是JSON规定的符号。msg_id字段表示消息的id号码,每条消息原则上应该使用递增的号码,msg_type字段表示消息的类型,msg_body字段表示消息的内容,其本身是json子对象,包含若干个特殊的字段。

需要注意,以上数据格式是字符串,因此在编程中直接写成ASCII字符串,注意字符串中的引号需要转义。

举个例子,对于battle_field_config类型的消息,其消息的格式如下:

"{\"msg_id\":0,\"msg_type\":\"battle_field_config\",\"msg_body\":{\"central_x\":0,\"central_y\":0,\"nswidth\":1000,\"wewidth\":1000}}"
  • 1

为了直观,我们写出格式化的形式(实际发送还是按照上面的紧凑形式,这个只是便于查看):

{
	"msg_id": 0,
	"msg_type": "battle_field_config",
	"msg_body": {
		"central_x": 0,
		"central_y": 0,
		"nswidth": 1000,
		"wewidth": 1000
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这条消息的编号为0,那么下个消息的编号应该为1,消息类型是battle_field_config,也就是战场总体配置,消息体包含战场中心坐标和战场南北、东西范围。

目前系统支持的消息有如下几类:

消息类型功能
battle_field_config战场中心坐标、战场范围
building_info增/删/缩放/移动一个建筑物
mountain_info增/删/缩放/移动一个山
plane_info增/删/移动一个飞机
weapon_info_missile增/删/移动一个导弹
weapon_info_net增/删/移动一个拦截网
weapon_info_bullet某飞机发射子弹
weapon_info_laser某飞机发射激光
explosion产生一个爆炸
ui_draw_lineui绘制-线
ui_draw_circleui绘制-椭圆
ui_draw_filledquadui绘制-填充矩形
ui_draw_textui绘制-文字
ui_destroyui删除
scene_draw_line3d绘制-线
scene_destroy3d删除
message提示信息

其中,JSON我们直接采用成熟的库,C++代码使用CJsonObject,C#代码使用SimpleJSON

4. C++部分

首先必须强调,这些东西可以比较容易地用任何语言替换,这里只是举例说明。

为了演示系统的效果,除了接口的部分,还实现了UDP通信、飞机模型(3dof)、导弹模型(3dof)的功能。

代码量应该不超过3000行,很多代码都是以前积累下来的。

UDP通信部分,直接使用了Windows下Socket通信方案,我把它稍微封装了下,使用起来非常简单,首先包含头文件comm_tools.h,然后:

 // 0. 定义数据接收线程函数,注意如果接收失败,则 num = -1
void socket_recv_thread_func(char* data, int num){...}
// 1. 实例化类,下面四种选一个
CSocketTool iSocketTool("127.0.0.1", 5000, TCP_CLIENT);	// TCP 客户端
CSocketTool iSocketTool("127.0.0.1", 5000, IP_CLIENT);	// UDP 客户端
CSocketTool iSocketTool(5000, TCP_SERVER);				// TCP 服务端
CSocketTool iSocketTool(5000, IP_SERVER);				// UDP 服务端
// 2. 连接
if (!iSocketTool.Connect()){cout << "socket connect failed." << endl; exit(0);}
// 3. 创建数据接收线程
if (!iSocketTool.CreateRecvThread(socket_recv_thread))
{cout << "recv thread create failed." << endl; exit(0);}
// 4. 在需要的地方发送数据
if (!iSocketClient.Send(data)){cout << "data send failed." << endl; exit(0);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

模型部分,采用了最简单的三自由度模型,使用过载进行控制,并使用龙格库塔法进行插值运算。
核心代码如下:

// 对于固定翼飞机
SPlaneModelState CPlaneModelIn3Dof_FixedWing::Run(double _nx, double _nz, double _ny)
{ // 下面的模型 nz 和 ny 和之前的定义是相反的,所以形参把顺序变了
	// 限制过载
	if (sqrt(_nx * _nx + _ny * _ny + _nz * _nz) <= this->dMaxOverload){
		// 过载柔性更新
		nx = alpha * nx + (1 - alpha) * _nx;
		ny = alpha * ny + (1 - alpha) * _ny;
		nz = alpha * nz + (1 - alpha) * _nz;
	}
	// 简化变量
	double v = sPlaneCurState.v;
	double atti[2] = {sPlaneCurState.pathpitch, sPlaneCurState.pathyaw};
	double K[5][4];
	double dAtt[3];
	double dPos[3];

	// 四阶龙格库塔法 - [pitch_dot, yaw_dot, pos_north, pos_up, pos_east]
	v = v + dTimeStep * G * nx;
	K[0][0] = G*(ny - cos(atti[0]))/v;
	K[1][0] = G*nz/v/cos(atti[0]);
	K[2][0] = v*cos(atti[0])*cos(atti[1]);
	K[3][0] = v*sin(atti[0]);
	K[4][0] = -v*cos(atti[0])*sin(atti[1]);

	K[0][1] = G*(ny - cos(atti[0] + K[0][0]*dTimeStep/2))/v;
	K[1][1] = G*nz/v/cos(atti[0] + K[0][0]*dTimeStep/2);
	K[2][1] = v*cos(atti[0] + K[0][0]*dTimeStep/2)*cos(atti[1] + K[1][0]*dTimeStep/2);
	K[3][1] = v*sin(atti[0] + K[0][0]*dTimeStep/2);
	K[4][1] = -v*cos(atti[0] + K[0][0]*dTimeStep/2)*sin(atti[1] + K[1][0]*dTimeStep/2);

	K[0][2] = G*(ny - cos(atti[0] + K[0][1]*dTimeStep/2))/v;
	K[1][2] = G*nz/v/cos(atti[0] + K[0][1]*dTimeStep/2);
	K[2][2] = v*cos(atti[0] + K[0][1]*dTimeStep/2)*cos(atti[1] + K[1][1]*dTimeStep/2);
	K[3][2] = v*sin(atti[0] + K[0][1]*dTimeStep/2);
	K[4][2] = -v*cos(atti[0] + K[0][1]*dTimeStep/2)*sin(atti[1] + K[1][1]*dTimeStep/2);

	K[0][3] = G*(ny - cos(atti[0] + K[0][2]*dTimeStep))/v;
	K[1][3] = G*nz/v/cos(atti[0] + K[0][2]*dTimeStep);
	K[2][3] = v*cos(atti[0] + K[0][2]*dTimeStep)*cos(atti[1] + K[1][2]*dTimeStep);
	K[3][3] = v*sin(atti[0] + K[0][2]*dTimeStep);
	K[4][3] = -v*cos(atti[0] + K[0][2]*dTimeStep)*sin(atti[1] + K[1][2]*dTimeStep);

	dAtt[0] = dTimeStep*(K[0][0] + 2*K[0][1] + 2*K[0][2] + K[0][3])/6.0;
	dAtt[1] = dTimeStep*(K[1][0] + 2*K[1][1] + 2*K[1][2] + K[1][3])/6.0;

	dPos[0] = dTimeStep*(K[2][0] + 2*K[2][1] + 2*K[2][2] + K[2][3])/6.0;
	dPos[1] = -dTimeStep*(K[3][0] + 2*K[3][1] + 2*K[3][2] + K[3][3])/6.0;
	dPos[2] = -dTimeStep*(K[4][0] + 2*K[4][1] + 2*K[4][2] + K[4][3])/6.0;

	// 更新飞机状态
	// 限速
	if ( v < 0 ) v = 0;
	if (v > this->dMaxSpeed) v = this->dMaxSpeed;
	sPlaneCurState.v = v;
	sPlaneCurState.pathpitch += dAtt[0];
	sPlaneCurState.pathyaw += dAtt[1];

	// 约束俯仰角,规范偏航角取值
	// if(fabs(sPlaneCurState.pathpitch) > PI/2 )sPlaneCurState.pathpitch = sign(sPlaneCurState.pathpitch)*PI/2;
	sPlaneCurState.pathpitch = AngleTrimInPI(sPlaneCurState.pathpitch);
	sPlaneCurState.pathyaw = AngleTrimInPI(sPlaneCurState.pathyaw);

	sPlaneCurState.vx = sPlaneCurState.v * cos(sPlaneCurState.pathpitch) * cos(sPlaneCurState.pathyaw);
	sPlaneCurState.vy = sPlaneCurState.v * cos(sPlaneCurState.pathpitch) * sin(sPlaneCurState.pathyaw);
	sPlaneCurState.vz = -sPlaneCurState.v * sin(sPlaneCurState.pathpitch);

	sPlaneCurState.pitch = sPlaneCurState.pathpitch;
	sPlaneCurState.yaw = sPlaneCurState.pathyaw;
	double sumN = sqrt(
		pow(nx, 2) + pow(ny, 2) + pow(nz, 2)
	);
	sPlaneCurState.roll = nz / (sumN > 0.01 ? sumN : 0.01) / 4.0 * PI;
	sPlaneCurState.roll = sign(sPlaneCurState.roll) * min(PI / 4.0, fabs(sPlaneCurState.roll));

	sPlaneCurState.x += dPos[0];
	sPlaneCurState.y += dPos[2];
	sPlaneCurState.z += dPos[1];

	xy_to_latlon(sPlaneCurState.x, sPlaneCurState.y, sPlaneCurState.lat, sPlaneCurState.lon);
	sPlaneCurState.alt = -sPlaneCurState.z;

	return this->sPlaneCurState;
}

// 对于旋翼飞机
SPlaneModelState CPlaneModelIn3Dof_RotorCraft::Run(double _vx, double _vy, double _vz, double _vyaw)
{
	// 限速
	if (sqrt(_vx * _vx + _vy * _vy + _vz * _vz) <= this->dMaxSpeed){
		// 机体速度柔性更新
		vx = alpha * vx + (1 - alpha) * _vx;
		vy = alpha * vy + (1 - alpha) * _vy;
		vz = alpha * vz + (1 - alpha) * _vz;
		_vyaw = AngleTrimInPI(_vyaw);
		vyaw = vyaw + (1 - alpha * 0.5) * AngleTrimInPI(_vyaw - vyaw);	// 0.5 的目的是加快偏航的变化速率
	}
	// 机体速度转换为地面速度
	this->sPlaneCurState.pathyaw += vyaw * this->dTimeStep;
	this->sPlaneCurState.vx = vx * cos(this->sPlaneCurState.pathyaw) - vy * sin(this->sPlaneCurState.pathyaw);
	this->sPlaneCurState.vy = vx * sin(this->sPlaneCurState.pathyaw) + vy * cos(this->sPlaneCurState.pathyaw);
	this->sPlaneCurState.vz = vz;
	this->sPlaneCurState.v = sqrt(
		pow(this->sPlaneCurState.vx, 2) + 
		pow(this->sPlaneCurState.vy, 2) + 
		pow(this->sPlaneCurState.vz, 2)
	);
	// 其它角度更新
	this->sPlaneCurState.pathpitch = 0;
	this->sPlaneCurState.pitch = 0;
	this->sPlaneCurState.yaw = this->sPlaneCurState.pathyaw;
	this->sPlaneCurState.roll = 0;
	this->sPlaneCurState.pathyaw = AngleTrimInPI(this->sPlaneCurState.pathyaw);
	this->sPlaneCurState.pathpitch = AngleTrimInPI(this->sPlaneCurState.pathpitch);
	this->sPlaneCurState.pitch = AngleTrimInPI(this->sPlaneCurState.pitch);
	this->sPlaneCurState.yaw = AngleTrimInPI(this->sPlaneCurState.yaw);
	this->sPlaneCurState.roll = AngleTrimInPI(this->sPlaneCurState.roll);
	// 位置更新
	this->sPlaneCurState.x += this->sPlaneCurState.vx * this->dTimeStep;
	this->sPlaneCurState.y += this->sPlaneCurState.vy * this->dTimeStep;
	this->sPlaneCurState.z += this->sPlaneCurState.vz * this->dTimeStep;
	xy_to_latlon(sPlaneCurState.x, sPlaneCurState.y, sPlaneCurState.lat, sPlaneCurState.lon);
	this->sPlaneCurState.alt = -this->sPlaneCurState.z;

	return this->sPlaneCurState;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126

消息构造部分,直接封装成为一个类,成员函数输入参数,输出生成的消息字符串,这个消息字符串直接用udp发送出去即可。类的定义为:

class CSimInterface{ // 所有的字符串都是用内置的静态字段
    public:
        CSimInterface();
        // 生成消息
        // 战场信息配置:战场中心坐标,南北范围,东西范围
        string MsgBattleFieldConfig(double central_x, double central_y, double nswidth = 10000, double wewidth = 10000);
        // 每次发送一个建筑的信息
        string MsgBuildingInfo(int id, string type, string status, double x, double y, double alt, double nswidth = 5, double wewidth = 20, double height = 50);
        // 每次发送一个山的信息
        string MsgMountainInfo(int id, string type, string status, double x, double y, double alt, double nswidth = 5, double wewidth = 20, double height = 50);       
        // 每次发送一个飞机的信息
        string MsgPlaneInfo(int id, string name, string type, string status, double x, double y, double alt, double pitch = 0, double roll = 0, double yaw = 0);          
        // 每次发送一个武器的信息
        string MsgWeaponInfo_Missle(int id, string name, string type, string status, double x, double y, double alt, double pitch = 0, double yaw = 0);
        // 每次发送一个武器的信息
        string MsgWeaponInfo_Net(int id, string name, string type, string status, double x, double y, double alt, double pitch = 0, double yaw = 0);
        // 每次发送一个武器的信息 : 方向向量是机体坐标系下的
        string MsgWeaponInfo_Bullet(string type, int parrent_id, double vec_x = 0, double vec_y = 0, double vec_z = 0);
        // 每次发送一个武器的信息 : 方向向量是机体坐标系下的
        string MsgWeaponInfo_Laser(string type, int parrent_id, double vec_x = 0, double vec_y = 0, double vec_z = 0);
        // 每次发送一个独立的爆炸信息,该爆炸立即起爆
        string MsgExplosion(double scale, double x, double y, double alt);
        // 每次发送一条UI绘图信息,用于自定义 UI 显示,以屏幕中心为原点
        string MsgUiDrawLine(int groupid, int startx, int starty, int endx, int endy, string color);
        string MsgUiDrawCircle(int groupid, int centerx, int centery, int radiusx, int radiusy, string color);
        string MsgUiDrawFilledQuad(int groupid, int p1x, int p1y, int p2x, int p2y, int p3x, int p3y, int p4x, int p4y, string color);
        string MsgUiDrawText(int groupid, int startx, int starty, string text, string color, int fontSize);
        string MsgUiGroupDestroy(int groupid);
        // 每次发送一条场景画线信息,用于航机规划结果的显示
        string MsgSceneDrawLine(int groupid, double startx, double starty, double startz, double endx, double endy, double endz, string color, double width);
        string MsgSceneLineGroupDestroy(int groupid);
        // 发送显示在状态栏的消息
        string MsgMessage(string message);
        // 解析消息 : 确认回令,暂时不需要使用
        int MsgReceived(string msg);

        int NewObjectId();
    private:
        int cur_msg_id;
        int cur_obj_id;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

此外,还需要写一些工具函数,比如WSG84坐标和经纬度的转换,机体坐标系和地面坐标系的转换等,具体可以参考飞控书籍。

5. U3D部分

这一块的工作量很大,大概涉及:场景的构思和搭建、模型的搜索修改和绘制、界面设计和交互模式设计、粒子特效设计、各种功能性脚本的C#代码实现。代码量在5500行左右吧,其中至少1500行代码是从各种地方摘过来的。

本部分就不详细介绍了,下面两个图给出了Hierarchy面板和Project面板中的情况。

层级结构(包含挂载的脚本):
在这里插入图片描述
工程资源:
在这里插入图片描述

6. 效果展示

远程配置建筑物,移动视角显示:
在这里插入图片描述
单机姿态演示:
在这里插入图片描述
单机轨迹演示:
在这里插入图片描述

比较全面的效果演示见b站视频

7. 总结

这个项目看起来比较复杂,实际开发起来感觉还好。最后达成的效果已经满足了自己的预想,实现的系统在开发过程中添加了很多之前没有想到的功能,总体来说是非常丰富的,适用于各种研究场景,还可以在这个基础上进行更多的扩展。

学习的过程要和做项目结合才能激励自己向前,反馈明确,目标明确,掌握好节奏,才能最终成事。

参考资料

(边做边搜索,参考的内容实在太多了,这里罗列一些最重要的吧,主要是教程和参考文档。)

C#

[1].微软C#入门
[2]..NET API

Unity

[1].中文手册
[2].英文手册
[3].资源商店
[4].教程1,教程2,教程3,教程4
[5].粒子特效
[6].Shader

其它

[1].3D模型

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号