本文介绍的程序使用SDL库,模拟蚁群找食物,背后的算法模型是简化的蚁群算法。我将采取自上而下的方式介绍。
SDL库介绍
安装
我在另一篇博客已经详细介绍了SDL库的安装与使用:C++实现图形化2048小游戏。这次要使用的是SDL2和SDL2_image两个子库。
设置编译参数
我使用的是VS Code,需要在task.json文件夹里设置args参数"-lSDL2",和"-lSDL2_image",代表编译时要使用到这2个库。
主函数介绍
在使用SDL库过程中,总体按照初始化SDL
→
\to
→ 初始化窗口
→
\to
→ 构建窗口渲染器
→
\to
→ 绘制图形
→
\to
→ 销毁的步骤。
在使用SDL_image库时,总体按照初始化
→
\to
→ 加载图片成表面
→
\to
→ 生成纹理
→
\to
→ 绘制图片的步骤。
int main(int argc, char* argv[]) {
int n;
cout << "请输入蚂蚁最大种群数:" << flush;
cin >> n;
// 初始化SDL显示图像功能
if (SDL_Init(SDL_INIT_VIDEO)) {
cerr << "SDL could not initialize! SDL_Error:" << SDL_GetError() << endl;
return 1;
}
// 初始化图片加载功能
if (!IMG_Init(IMG_INIT_PNG)) { //初始化IMG组件失败
cerr << "SDL_image could not initialize! SDL_image Error:" << IMG_GetError() << endl;
IMG_Quit();
return 1;
}
// 初始化窗口
SDL_Window* window = SDL_CreateWindow("蚁群模拟", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, N*DIM, N*DIM, SDL_WINDOW_SHOWN);
if (window == NULL) { //初始化窗口失败
cerr << "Window could not be created! SDL_Error:" << SDL_GetError() << endl;
IMG_Quit();
SDL_Quit();
return 1;
}
// 构建窗口渲染容器
SDL_Renderer* rend = SDL_CreateRenderer(window, -1, 0);
if (!rend) {
cerr << "Unable to render! SDL_Render Error:" << SDL_GetError() << endl;
SDL_DestroyWindow(window);
IMG_Quit();
SDL_Quit();
return 1;
}
//初始化渲染器
SDL_SetRenderDrawColor(rend, 255, 255, 255, 255); //白色
SDL_RenderClear(rend);
//加载第1种图片
SDL_Surface *image0 = IMG_Load("ant0.png"); //打开图片
if (image0 == NULL) { //加载图片失败
cerr << "Unable to load image! SDL_image Error:" << IMG_GetError() << endl;
SDL_DestroyRenderer(rend);
SDL_DestroyWindow(window);
IMG_Quit();
SDL_Quit();
return 1;
}
//加载第2种图片
SDL_Surface *image1 = IMG_Load("ant1.png");
if (image1 == NULL) { //加载图片失败
cerr << "Unable to load image! SDL_image Error:" << IMG_GetError() << endl;
SDL_DestroyRenderer(rend);
SDL_DestroyWindow(window);
IMG_Quit();
SDL_Quit();
return 1;
}
//生成纹理
SDL_Texture *imageTexture0 = SDL_CreateTextureFromSurface(rend, image0);
SDL_Texture *imageTexture1 = SDL_CreateTextureFromSurface(rend, image1);
//初始化伪随机数生成器
srand(time(NULL));
//初始化蚂蚁之家
Home::x = rand() % (N - 2) + 1; //不要在边沿放置
Home::y = rand() % (N - 2) + 1;
Home::render = rend;
Home::texture0 = imageTexture0;
Home::texture1 = imageTexture1;
//初始化地面
Ground::INFO_MAX = n / 2 * Ant::INFO;
Ground::block_home[Home::x * N + Home::y] = Ground::INFO_MAX; //蚂蚁之家的信息素浓度
Ground::render = rend;
//生成食物
int foodAmount = 5; //食物数量
for (int i = 0; i < foodAmount; i++) {
int p = rand() % (N * N);
if (p == Home::x * N + Home::y) continue; //不能放置在蚂蚁窝
cout << "生成食物:" << p / N << "," << p % N << endl;
Foods::position[p] = 200; //随机位置,默认大小为200
}
Foods::render = rend;
//一次性生成蚂蚁种群的一半
for (int i = 0; i < n/2; i++) {
Home::addAnt(); //手动添加蚂蚁
}
//增加用户交互性
SDL_Event event;
//刷新
while (true) {
SDL_PollEvent(&event); //等待事件
if (event.type == SDL_QUIT) { // 退出
break;
} else if (event.type == SDL_KEYDOWN) { // 按键
//防抖
while(true) {
SDL_PollEvent(&event); //等待事件
if ((event.type == SDL_KEYUP)) break;
}
switch (event.key.keysym.sym) { //检测按了哪个方向键
case SDLK_SPACE: //按下了空格键
if (Home::ants.size() < n) {
Home::addAnt(); //手动添加蚂蚁
if (Home::ants.size() % 2 == 0) { //保持奇数个蚂蚁
Home::addAnt();
}
cout << "蚂蚁数量:" << Home::ants.size() << endl;
}
break;
}
}
display(rend); //显示
Home::explore(); //让没食物的蚂蚁探索
}
//销毁
SDL_FreeSurface(image0);
SDL_FreeSurface(image1);
SDL_DestroyTexture(imageTexture0);
SDL_DestroyTexture(imageTexture1);
SDL_DestroyRenderer(rend);
SDL_DestroyWindow(window);
IMG_Quit();
SDL_Quit();
}
这里有个display函数,就是调用各个类的显示,先画地面,再画食物,最后画蚂蚁窝和蚂蚁:
//全局显示函数
void display(SDL_Renderer * rend) {
Ground::show();
Foods::show();
Home::show(); //画蚂蚁窝和蚂蚁
SDL_RenderPresent(rend); //刷新画布
SDL_Delay(FREQUENCE); //延时,定义的宏,随便填,根据人眼分辨机能,可以设置为20ms
}
数据结构
蚂蚁之家类Home,采用静态类的模式,有3个静态函数,分别都会调用蚂蚁类Ant相应的函数:
//ant.h
//蚂蚁之家类=======================================
class Home {
public:
static SDL_Renderer *render; //屏幕渲染器指针
static SDL_Texture *texture0, *texture1; //无食物和有食物状态的纹理
static deque<Ant> ants; //所有蚂蚁
static int x, y; //位置
static void addAnt(); //添加蚂蚁
static void show(); //图像显示蚂蚁之家和蚂蚁
static void explore(); //模拟蚂蚁探索
};
蚂蚁类Ant,里面设置多个坐标,(x,y)代表蚂蚁在数组坐标,其实就是地面坐标,一会介绍Ground类包含了地板数组;(px,py)代表图像显示坐标,因为要实现蚂蚁移动的动画,它代表的是绘制蚂蚁图片的位置;(dx,dy)代表当前蚂蚁的目标坐标,也是数组坐标。其他的请看注释:
//ant.h
//蚂蚁类===============================
class Ant {
private:
SDL_Renderer *render; //屏幕渲染器指针
SDL_Texture *texture0, *texture1; //无食物和有食物状态的纹理
public:
int x, y; //当前位置
int px, py; //图像显示位置
int dx, dy; //目标格子坐标
int direction; //方向
bool food; //有无食物
int info; //信息素浓度
static double INFO; //最大信息素浓度
static int VOCITY; //速度
static double IMPORTANT; //蚂蚁信息重要程度因子
//基础函数部分
Ant(int, int, SDL_Renderer *, SDL_Texture *, SDL_Texture *); //构造函数
void emitInfo(); //在当前位置留下信息素
void directionToPos(int, int *, int *); //将方向转化成下步坐标
void sniffDirection(); //获取周围空间信息素并赋予方向,但不能走回头路,且有机率开创新路径
void moveToNext(); //移动蚂蚁至下一个砖块
void show(); //图像显示函数
//高级函数部分
void explore(); //探索
};
地面类,想在地板上根据蚂蚁信息素浓度标记地板颜色,所以使用了INFO_MAX用于表示信息素最大可以到的浓度,定义了2个数组block_home表示蚂蚁释放的回家信息素,它们外出找食物时会在地板上留下这种信息素,另1个数组block_food表示蚂蚁找到食物后释放的信息素,在回家的途中会进行释放:
//ant.h
//地面类===============================
class Ground {
public:
static int INFO_MAX; //最大信息浓度
static SDL_Renderer *render; //屏幕渲染器指针
static double block_home[N*N]; //共10X10块地砖,每块的数值是蚂蚁释放家信息素的浓度,而坐标就是地砖的坐标
static double block_food[N*N]; //释放的食物信息素
static void show(); //地板显示函数
static void evade(); //气味消散函数
};
最后一个是食物类Foods,比较简单:
//ant.h
//食物类==================================
class Foods {
public:
static SDL_Renderer *render; //屏幕渲染器指针
static int position[N*N]; //共16X16块地砖可能有食物,其数值代表食物大小
static void show(); //食物显示函数
};
数学模型介绍
推荐看一篇文章数学建模学习笔记(一)蚁群算法(MATLAB),因为我设置的场景是正方形砖块平铺,蚂蚁也只有上、下、左、右4个方向,且距离都相等设为1,于是我对该模型进行了简化:
P
k
=
τ
k
α
⋅
1
∑
k
=
1
4
τ
k
α
⋅
1
P_k=\frac{\tau_k^\alpha \cdot 1}{\sum_{k=1}^4 \tau_k^\alpha \cdot 1}
Pk=∑k=14τkα⋅1τkα⋅1,其中
α
\alpha
α是信息素重要程度,我在程序里设置成2,
τ
\tau
τ 是该方向上邻近的砖块所含的信息素浓度,
P
k
P_k
Pk就是蚂蚁选择方向k的概率。
类的实现
参数设置
所有参数统一在ant.h头文件中定义:
#define N 10 //房间每个边上地砖的块数
#define DIM 100 //地砖尺寸
#define FREQUENCE 20 //动画刷新频率
#define _INFO_ 1000 //单个蚂蚁最大信息素浓度
#define _INFO_DELT_ 50 //信息素递减浓度
#define _IMPORTANT_ 2 //信息素浓度重要程度
#define _EVADE_ 0.99 //挥发系数
_INFO_DELT_代表蚂蚁每移动一格信息素浓度递减的数值,也就是蚂蚁走的越远信息素浓度越小,直到没有,这样可以有效防止蚂蚁陷入局部最优解。_INFO_蚂蚁分别在找到食物和把食物放回家这2种状态下可以补充信息素回到_INFO_的数值。_IMPORTANT_就是数学模型里的 α \alpha α,于是概率方程就变成了 P k = τ k 2 ∑ k = 1 4 τ k 2 P_k=\frac{\tau_k^2}{\sum_{k=1}^4 \tau_k^2} Pk=∑k=14τk2τk2._EVADE_气味消散系数,定义蚂蚁每移动1格为1秒,地面上的所有信息素会随时间每秒变成原来的0.99倍。
蚂蚁之家类实现
比较简单,除了蚂蚁窝的位置、显示蚂蚁窝外,基本上就是调用各个蚂蚁相应的函数:
//home.cpp
//===============蚂蚁之家实现部分============
void Home::addAnt() {
Home::ants.push_back(Ant(Home::x, Home::y, Home::render, Home::texture0, Home::texture1));
}
//绘制蚂蚁之家
void Home::show() {
SDL_Rect rect = {Home::y * DIM + DIM/4, Home::x * DIM + DIM/4, DIM/2, DIM/2}; //用矩形代替
SDL_SetRenderDrawColor(Home::render, 0, 0, 255, 255); //用蓝色
SDL_RenderFillRect(Home::render, &rect);
// int k = 0;
for (deque<Ant>::iterator it = Home::ants.begin(); it != Home::ants.end(); it++) {//画蚂蚁
// cout << "ant" << k++ << ":" << endl;
it->show();
}
}
//下达探索指令
void Home::explore() {
for (deque<Ant>::iterator it = Home::ants.begin(); it != Home::ants.end(); it++) {
it->explore();
}
}
蚂蚁类实现
//ant.cpp
double Ant::INFO = _INFO_; //信息素浓度
int Ant::VOCITY = DIM / 10; //蚂蚁的速度
double Ant::IMPORTANT = _IMPORTANT_; //蚂蚁信息重要程度因子
//=========================蚂蚁实现部分============================
//构造函数
Ant::Ant(int x, int y, SDL_Renderer * rend, SDL_Texture *t0, SDL_Texture *t1):x(x), y(y), dx(x), dy(y),
render(rend), texture0(t0), texture1(t1) {
cout << "蚂蚁出动:" << this->x << "," << this->y << endl;
this->px = this->x * DIM + DIM/4;
this->py = this->y * DIM + DIM/4;
this->direction = rand() % 4; //随机方向
this->food = false;
this->info = Ant::INFO;
}
//省略部分代码
//...
//蚂蚁嗅探方向
void Ant::sniffDirection() {
int randD[4] = {-1, -1, -1, -1}; //随机方向存储器
double infoD[4] = {}; //各方向上信息素浓度
double infoSum = 0; //各方向上总浓度
int cur = 0; //随机方向存储器尺寸指针
for (int d = 0; d < 4; d++) { //遍历4个方向
if (d == (this->direction + 2) % 4) continue; //不能是反方向回头路
int dx, dy;
this->directionToPos(d, &dx, &dy);
if (dx < 0 || dy < 0 || dx >= N || dy >= N) continue; //越界了
randD[cur] = d; //标记可行方向
if (this->food) { //根据是否有食物判断嗅探什么类型的信息素
infoD[cur] = pow(Ground::block_home[dx * N + dy], Ant::IMPORTANT);
} else {
infoD[cur] = pow(Ground::block_food[dx * N + dy], Ant::IMPORTANT);
}
infoSum += infoD[cur];
cur++;
} //end of for
assert(cur); //断言必有可行方向
if (infoSum < 0.5) { //周围没有信息素
this->direction = randD[rand() % cur]; //随机方向
} else {
for (int i = 0; i < cur; i = (i + 1) % cur) { //轮盘赌,直到选出方向
if (rand() % 100 < infoD[i] / infoSum * 100) { //有概率选择了这一方向,使用了数学概率模型
this->direction = randD[i];
break;
}
}
}
this->directionToPos(this->direction, &(this->dx), &(this->dy)); //更新目标方格坐标
// cout << "改变方向:" << this->direction << " " << this->dx << "," << this->dy << endl;
}
//蚂蚁向下一方块移动
void Ant::moveToNext() {
static int i = 0;
// cout << "边长" << sqrt((dx - x) * (dx - x) + (dy - y) * (dy - y) * 1.0) * DIM << endl;
if (i++ < sqrt((dx - x) * (dx - x) + (dy - y) * (dy - y) * 1.0) * DIM / Ant::VOCITY) { //中途用实际长度衡量
if (dx != x) px += ((dx - x) / abs(dx - x)) * Ant::VOCITY;
if (dy != y) py += ((dy - y) / abs(dy - y)) * Ant::VOCITY;
}
if (i >= sqrt((dx - x) * (dx - x) + (dy - y) * (dy - y) * 1.0) * DIM / Ant::VOCITY) { //到达终点
this->x = this->dx; this->y = this->dy; //更新当前坐标
this->px = this->x * DIM + DIM/4; this->py = this->y * DIM + DIM/4; //更新显示坐标
Ground::evade(); //气味挥发
this->emitInfo(); //蚂蚁在新的地砖释放信息素,根据是否有食物要释放不同种类的信息素
i = 0; //初始化计数
}
}
//蚂蚁探索函数,细分显示步骤
void Ant::explore() {
if (this->x == this->dx && this->y == this->dy) { //到达指定方格,不在中间移动过程中
if (Foods::position[this->x*N+this->y]) { //找到了食物
if (this->food == false) {
// cout << "找到食物:" << this->x << "," << this->y << endl;
this->food = true;
this->info = Ant::INFO; //重生信息素
this->emitInfo(); //释放食物信息素
this->direction = (this->direction + 2) % 4; //反方向
Foods::position[this->x*N+this->y]--; //食物少1
}
} else if (Home::x == this->x && Home::y == this->y) { //找到家了
if (this->food == true) { //有食物
// cout << "放下食物" << endl;
this->food = false;
this->direction = (this->direction + 2) % 4; //反方向
this->info = Ant::INFO; //重生信息素
}
}
this->sniffDirection();
}
this->moveToNext();
}
地面类及食物类的实现
为了对地板的颜色标记自然,不出现反差太大的情况,使用了log函数,即信息素浓度越大,颜色反差越小,使显示更渐近自然,这里只对回家信息素进行标记,使用max函数是为了防止数值越界成负值:
//gound.cpp
//===============地面实现部分===============
void Ground::show() {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
SDL_Rect rect = {j * DIM, i * DIM, DIM, DIM};
SDL_SetRenderDrawColor(Ground::render, max(0,(int)(log(Ground::block_home[i*N+j])/log(Ground::INFO_MAX)*100))+125, max(0,(int)(log(Ground::block_home[i*N+j])/log(Ground::INFO_MAX)*100))+125,0,255); //越浓越鲜艳
SDL_RenderFillRect(Ground::render, &rect);
SDL_SetRenderDrawColor(Ground::render, 0, 0, 0, 255); //画边框
SDL_RenderDrawRect(Ground::render, &rect);
}
}
}
//气味消散函数
void Ground::evade() {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (i == Home::x && j == Home::y) Ground::block_home[i*N+j] = Ground::INFO_MAX; //蚁穴回家气味不减
Ground::block_food[i*N+j] *= _EVADE_;
Ground::block_home[i*N+j] *= _EVADE_;
if (Ground::block_food[i*N+j] < 0.5) {
Ground::block_food[i*N+j] = 0;
}
if (Ground::block_home[i*N+j] < 0.5) {
Ground::block_home[i*N+j] = 0;
}
}
}
}
食物类的实现更简单了,使用红色方块代替:
//================食物实现部分==============
void Foods::show() {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (Foods::position[i*N+j]) { //有食物
SDL_Rect rect = {j * DIM + DIM/4, i * DIM + DIM/4, DIM/2, DIM/2}; //用矩形代替
SDL_SetRenderDrawColor(Foods::render, 255, 0, 0, 255); //用红色
SDL_RenderFillRect(Foods::render, &rect);
}
}
}
}
编译环境设置
我使用的是VS Code,Linux系统,要多文件编译,在task.json文件中编辑如下:
{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++ 生成活动文件",
"command": "/usr/bin/g++",
"args": [
"-fdiagnostics-color=always",
"-g",
"-lSDL2",
"-lSDL2_image",
"main.cpp",
"ant.cpp",
"home.cpp",
"ground.cpp",
"-o",
"${fileDirname}/${fileBasenameNoExtension}"
],
"options": {
"cwd": "${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "调试器生成的任务。"
}
],
"version": "2.0.0"
}
效果展示
我使用了128只蚂蚁作为演示模拟蚁群

9569

被折叠的 条评论
为什么被折叠?



