C++模拟外卖派送系统(图形库)

引言

之前接的一个单,做的一个图形化的外卖派送系统,是一道课程设计题,要求使用动画时刻显示每个外卖小哥的位置。程序自己对订单进行调度,订单信息可以从文件 sales.txt 中读取,也可以通过鼠标拖拽进行派单:起点是商家,终点是食客。

题目

任务概述

你开了一家外卖快递服务店,负责一个区域内的外卖订单接收和餐食快递.你有一笔启动 资金,可以招募外卖骑手帮你送餐,来赚取快递费.但你也会面临风险,本区域的订单你都有义 务接收,不能拒绝,若拒单政府就会吊销你的营业执照;但如果接收多个订单后,因为骑手来不 及送,导致某个订单超时未送达,客户会投诉,你会被罚款. 因此,你的任务就是制定聪明的调度策略,避免拒单被吊销营业执照(人工调度在订单高 峰期可能会来不及接收),避免因为罚款而破产,并且尽可能赚更多的钱。

规则一、负责的区域

你负责的外卖派送区域如下图所示:

约束 1-1: 你的区域必须是一个 99 的方格,每格是一个房间,既可以是下订单的食客 家,也可以是接单的餐馆。
约束 1-2: 方格之间的 88 条街道是骑手唯一可走的道路;骑手停在方格的上下左右街道,即算抵达。
约束 1-3: 每个方格的宽高都一样,即骑手走过每个方格的距离一样,速度也一样,约 定为骑手每走过一个方格花费 1 个时间单位,骑手从最左边直达最右边花费时间 8 个时间 单位,即拐弯不花时间,经过路口不花时间。每个时间单位暂定为 20 秒,后续可能会统一 修改。
约束 1-4: 为了记录房间和骑手位置,坐标系定为17*17,约定左上角的房间逻辑坐标 为(0,0),右下角房间的逻辑坐标为(16,16)。横纵坐标一奇一偶的都是路,坐标都是偶数的 是房间,坐标都是奇数的是路口。

规则二、外卖订单格式和输入方式

约束 2-1: 你负责的外卖派送区域内,发起的任何订单都必须接收;如果订单发起后, 3 个时间单位内没有派单给现有骑手,则视为拒单,你将被吊销营业执照,运营终止。
约束2-2: 外卖订单的信息格式是一个四元组(n序号,t 下单时间,餐馆坐标,食客坐标);具 体格式规范如下,必须按此规范执行。(注:/表示前后数据分隔的含义,实际中不出现) 序号/分隔符空格 /下单时间/分隔符空格/餐馆坐标 x/分隔符空格/餐馆坐标 y/分隔 符空格/食客坐标 x/分隔符空格/食客坐标 y/回车符/n
约束 2-3: 外卖订单的输入方式,要求一套程序支持以文本文件格式的输入方式,和鼠 标操作的输入方式两种。两种方式不会同时生效。 文件输入方式:系统启动后检查当前目录下是否有标准文件名“sales.txt”,若有则自动 进入订单强制文件输入方式。sales.txt 文件必须遵从约束 2-2 规定的文件格式。 鼠标操作方式:系统启动后检查当前目录找不到标准文件“sales.txt”,则自动进入订单 强制鼠标输入方式。系统运行期间,鼠标点中任意方格再拖拽至另一方格,即算完成从餐馆 到食客的订单发起动作,系统必须实时记录并进行派单。

规则三、运营规则

约束 3-1:系统开始运营时,你有 1000$作为运营资本;
约束 3-2:你必须有骑手才能接单,招聘一位骑手需投资 300$; 只要你有钱,骑手数量 不限;在系统运营的整个期间,你都可以随时招聘骑手,但必须有足够的钱,不能拖欠;
约束 3-3:招聘骑手的发起方式只能是两种方式,一种是你的调度程序自动发起,另一 种是做成输入按钮,鼠标点击的方式发起。你可以只实现一种,也可以两种都实现,两种都 实现的好处是人工调度比自动调度聪明。
约束 3-4:接单的方式只能是将其按顺序派给指定骑手。接单的操作可以是人工接单, 也可以是程序按调度策略自动接单。人工接单的操作比较复杂,需要用鼠标逐个选中现有未 处理订单,将其分配给某个骑手。
约束 3-5:所有骑手的初始位置必须是同一位置;但起始位置需要在你的程序中自行设 定。骑手初始位置的设定可以在程序中写死,也可以在系统启动时修改设置,两种均可。注 意:如果骑手初始在左上角,那么立刻接到一个(右下角>左上角)的订单时,可能超时。
约束 3-6:每个订单从下单时间开始,要求在 30 个时间点内完成服务(先后抵达餐馆 和食客家),否则算超时。满 30 个时间点订单若没有完成结单,客户会投诉导致立刻罚款 50$; 满 60 个时间点订单若没有完成结单,属于恶意废单立刻破产。
约束 3-7:每完成一单且不超时,可收入 10$。
约束 3-8:无须考虑骑手负载限制,一位骑手可以带无限外卖;但超时未达要按约束 36 条处罚。
约束 3-9:负债即破产!一旦破产,即刻停止运营,系统盘点每位骑手的接单数、完成 数、超时数。
约束 3-10:系统运营期间,至少每个时间单位更新一次,显示当前钱数、每位骑手的位 置、接单数、完成数、超时数。

规则四、技术框架

约束 4-1:程序代码主体必须是 C 语言编写。 约束 4-2:界面部分,允许使用 C++的动画包。
建议:派单的基础策略
策略 x-1:首先按下单时间顺序,将新订单放入待处理队列。
策略 x-2:根据骑手数量,将区域划分为几块,分给每个骑手,注意预先保留一个跨区 域订单骑手。
策略 x-3:派单时,从待处理队列取出队首订单,判断属于哪个区域,就分给哪个骑手, 分派后此单出待处理队列,加入对应骑手的待送达队列;餐馆和食客不在一个区域的订单, 分给跨区域骑手。
策略 x-4:当跨区域骑手的待派送队列中元素超过预警值时(例如 10 个),再分出一 个骑手作跨区域骑手,剩下骑手重新划分区域,更改仅对后续订单生效,已分配订单受影响。 注意:预警值是你自己在程序中设定的,根据经验设置。
策略 x-5:所有单派完后,开始轮流对所有骑手的待派送队列进行优化。优化策略是: 取出队首订单,作为当前目标点;计算骑手当前位置和当前目标点的区域范围,然后扫描队 列后续订单中的所有可达目标点(见后续解释),筛选出属于此区域内的,设计出合理的行 走路线,只要按此路线当前目标点不超时,即可插入到队首目标点之前。 为了完成上述优化策略,骑手的待派送队列要把每个订单拆成 A、B 两个任务,每个元 素都包含订单号、A 或 B 标志,坐标位置点,下单时间,是否可达,这五个数据。A 任务没有约束条件,任何时间都属于可达目标点;B 任务,默认不可达,只有在 A 任务完成时,扫描队列将 A 任务对应订单号的 B 任务改为可达。策略 x-5 附加动作:当骑手的当前位置抵达待派送队列队首元素,队首元素出队,执行 任务撤销处理,若是 A 任务,修改队列中对应 B 任务为可达; 若是 B 任务,计算是否超时,超时罚款,最后将该骑手的完成订单数+1。

可视化界面

界面用的是图形库实现的,而不是通常上所说的GUI界面。这里用的是ege库,之前我也没有接触过。有很多其他的C++图形库也都不错。

动画的简单原理:一帧一帧的图像,只要屏幕刷新的够快就会有动画的效果。
其实只要理解动画的基本原理,就能很轻松的做出动画界面。
最终做出来的动画效果:
1.鼠标进行拖拽,起点是商家,终点是食客。

2.外卖小哥到达商家后,会出现一个美女头像。外卖小哥到达目的地后,会出现一个对勾。

代码

main.cpp

#include "head.h"
#include "control.cpp"
#include "done.cpp"
#include "input.cpp"
#include "output.cpp"
#define TNUM 3 //设置每时间单位对应的秒值

int main()
{
    init();          /*初始化:读取配置信息;*/
    get_fileInput(); /*读取输入文件,将订单都写入订单队列中*/
    init_draw();     //初始化画图面板
    while (!exits)
    {
        if (MONEY < 0) /*负资产即破产*/
            exits = 1;
        rider_change(MONEY);     /*买骑手*/
        init_message(rider_num); /*每次循环再次初始化停靠等信息*/
        orderOccur();            /*根据当前时间单位,激活已到下单时间的订单*/
        control();               /*控制:调度算法执行*/
        state_trans();           /*自动机:系统运行状态变化*/
        rider_arrive();          /*判断是否结单*/
        update_draw();
        print_message();           /*输出:状态刷新和事件提示*/
        cnt_t++; /*time_count();*/ /*计算程序运行时间,并共享给各模块*/
        if (automode)
        {
            if (x != 0 && order[x].number == 0) /*无订单时结束*/ /*真的结束是无单可派无单可送*/ /*这个if只能保证无单可派*/
            {
                int v, Q = 0;
                for (v = 0; v < rider_num; v++) /*执行for循环后Q依然为0则无单可送*/
                    if (rider[v].number != 0)
                    {
                        Q = 1;
                        break;
                    }
                if (Q == 0)
                    break; /*结束循环*/
            }
        }
    }
    system("pause");
    return 0;
}

int rider_arrive()
{
    int i;
    for (i = 0; i < rider_num; i++)
    {
        if (rider[i].number >= 1)
            if (flag[i] >= 2 && rider[i].x2[0] == state.rider_x[i] /*&&rider[i].y2[0]==state.rider_y[i]+1||rider[i].y2[0]==state.rider_y[i]-1*/)
            {
                flag[i] = 0;                         /*状态值回归为0,准备进行下一单的配送*/
                MONEY += 10;                         /*收益+10*/
                state.finish++;                      /*完成单数+1*/
                message[i].finish = rider[i].num[0]; /*结单号*/
                message[i].x1 = order[message[i].finish - 1].x2;
                message[i].y1 = order[message[i].finish - 1].y2;
                rider_remove(i, rider[i].number); /*销去当前已派送单信息,并把仍未派送单的信息往前移一位*/
                state_remove(i, rider[i].number); /*销去当前已派送单所耗时间,并把仍未派送单的信息往前移一位*/
                rider[i].number--;                /*骑手m的待送单-1*/
            }
    }
}

int rider_remove(int n, int num) /*销单*/ /*n为当前骑手,num为骑手n的接单数*/
{
    int g;
    for (g = 0; g < num - 1; g++) /*销去当前已派送单信息,并把仍未派送单的信息往前移一位*/
    {
        rider[n].num[g] = rider[n].num[g + 1];
        rider[n].time[g] = rider[n].time[g + 1];
        rider[n].x1[g] = rider[n].x1[g + 1];
        rider[n].y1[g] = rider[n].y1[g + 1];
        rider[n].x2[g] = rider[n].x2[g + 1];
        rider[n].y2[g] = rider[n].y2[g + 1];
    }
    rider[n].num[num - 1] = 0;
    rider[n].time[num - 1] = 0;
    rider[n].x1[num - 1] = 0;
    rider[n].y1[num - 1] = 0;
    rider[n].x2[num - 1] = 0;
    rider[n].y2[num - 1] = 0;
}

int state_remove(int n, int num) /*销单*/ /*n为当前骑手,num为骑手n的接单数*/
{
    int g;
    for (g = 0; g < num - 1; g++) /*销去当前已派送单所耗时间,并把仍未派送单的信息往前移一位*/
        state.time[n][g] = state.time[n][g + 1];
    state.time[n][num - 1] = 0;
}

int rider_change(int money) //根据钱数再次购买骑手并重新分配订单
{
    if (money >= 350) /*每当钱数超过350,再次购买一个骑手*/
    {
        MONEY -= 300;
        rider_num++;
    }
}

int init_message(int num)
{
    int u, U;
    for (u = 0; u < num; u++)
    {
        message[u].current = 0;
        message[u].finish = 0;
        message[u].repeat = 0;
        message[u].x1 = -1;
        message[u].y1 = -1;
        for (U = 0; U < 10; U++)
            message[u].overtime[U] = 0;
    }
}

void init_draw() //初始化画图面板
{
    //初始化画板 ,1000*690,690*690,9*50 + 8*30
    initgraph(SCR_WIDTH, SCR_HEIGHT);
    setbkcolor(EGERGB(0xFF, 0xFF, 0xFF));
    //文字颜色
    setcolor(BLACK);
    setfont(25, 0, "方正喵呜体");

    //要使用特殊格式化字符请用outtextrect
    char text[100];
    if (automode)
        sprintf(text, "浩博制作\n\n时间:%d\n\n钱:%d\n\n接单数:%d\n\n完成数:%d\n\n超时数:%d\n\n骑手数量:%d\n\n从文件读取信息", cnt_t, MONEY, state.accept, state.finish, state.overtime, rider_num);
    else
        sprintf(text, "浩博制作\n\n时间:%d\n\n钱:%d\n\n接单数:%d\n\n完成数:%d\n\n超时数:%d\n\n骑手数量:%d\n\n鼠标拖拽房子派单", cnt_t, MONEY, state.accept, state.finish, state.overtime, rider_num);
    outtextrect(760, 160, 200, 400, text);

    //设置文字背景填充方式为透明,默认为OPAQUE不透明
    setbkmode(TRANSPARENT);

    PIMAGE house = newimage();
    getimage(house, "House2.ghb", 10, 10);
    PIMAGE rider = newimage();
    getimage(rider, "Rider4.ghb", 10, 10);

    int t = 1;
    putimage(132, 132, rider);
    putimage(212, 212, rider);
    for (int y = 0; y < 9; ++y)
    {
        for (int x = 0; x < 9; ++x)
        {
            //把house整个,画在指定的坐标上,左上角对齐这个坐标
            putimage(x * 80, y * 80, house);
        }
    }
}

void update_draw() //更新画面
{
    PIMAGE house = newimage();
    getimage(house, "House2.ghb", 10, 10);
    PIMAGE rider = newimage();
    getimage(rider, "Rider4.ghb", 10, 10);
    PIMAGE ok = newimage();
    getimage(ok, "OK2.ghb", 10, 10);
    PIMAGE Love = newimage();
    getimage(Love, "Love2.ghb", 10, 10);
    int a = 2;
    static int startX, startY, isdown = 0, t1 = 0, t2 = 0, t1x, t1y, t2x, t2y, f = 0;
    mouse_msg msg;
    setlinestyle(0);
    setlinewidth(5); // 设置当前线宽
    while (a--)      //一次更新半个单位时间的画面,所以要执行两次
    {
        for (int t = 0; t < TNUM * 80 / 2; t++, delay_fps(80)) //delay_fps(80)就是1秒80次,每执行一次 delay_fps(x)就延时1/x秒
        {
            cleardevice();              //清屏,只要够快就会有动画效果
            for (int y = 0; y < 9; ++y) //放房子
                for (int x = 0; x < 9; ++x)
                    putimage(x * 80, y * 80, house);
            if (!automode) //鼠标模式
            {
                if (mousemsg())
                {
                    msg = getmouse();
                    switch (msg.msg)
                    {
                    case mouse_msg_down:
                        startX = msg.x;
                        startY = msg.y;
                        t1x = msg.x;
                        t1y = msg.y;
                        t1 = 200;
                        isdown = 1;
                        setfillcolor(RED);
                        setcolor(RED);
                        pieslice(msg.x, msg.y, 0, 360, 10);
                        printf("mouse down: %d, %d\n", msg.x, msg.y);
                        order[f].x1 = (msg.y) / 80 * 2;
                        order[f].y1 = (msg.x) / 80 * 2;
                        //order[f].number,&order[f].time,order[f].x2,&order[f].y2
                        break;
                    case mouse_msg_up:
                        isdown = 0;
                        t2x = msg.x;
                        t2y = msg.y;
                        t2 = 160;
                        setcolor(BLUE);
                        line(startX, startY, msg.x, msg.y); //绘制当前点和按下的点
                        setfillcolor(GREEN);
                        setcolor(GREEN);
                        pieslice(msg.x, msg.y, 0, 360, 10);
                        printf("mouse up: %d, %d\n", msg.x, msg.y);
                        order[f].x2 = (msg.y) / 80 * 2;
                        order[f].y2 = (msg.x) / 80 * 2;
                        if (order[f].x1 <= 16 && order[f].x2 <= 16 && order[f].y1 <= 16 && order[f].y2 <= 16)
                        {
                            order[f].time = cnt_t;
                            order[f].number = f + 1;
                            f++;
                        }
                        break;
                    }
                }
                if (isdown)
                {
                    int mx, my;
                    mousepos(&mx, &my); //鼠标位置
                    setcolor(BLUE);
                    line(startX, startY, mx, my); //绘制当前点和按下的点
                }
                if (t1-- > 0)
                {
                    setfillcolor(EGERGB(128, 50, 128));
                    setcolor(EGERGB(128, 50, 128));
                    pieslice(t1x, t1y, 0, 360, 10);
                }
                if (t2-- > 0)
                {
                    setfillcolor(GREEN);
                    setcolor(GREEN);
                    pieslice(t2x, t2y, 0, 360, 10);
                }
            }
            //刷新每个骑手在图像上的坐标
            for (int i = 0; i < rider_num; i++)
            {
                if (t % (TNUM * 80 / 2 / 40) == 0)
                {
                    if (state.rider_x[i] % 2 == 0 && state.rider_y[i] % 2 == 1) //纵向道路
                    {
                        if (state.rider_x[i] < state.rider_nextx[i])
                        {
                            state.rider_Y[i] += 1;
                        }
                        else if (state.rider_x[i] > state.rider_nextx[i])
                        {
                            state.rider_Y[i] -= 1;
                        }
                    }
                    else if (state.rider_x[i] % 2 == 1 && state.rider_y[i] % 2 == 0) //横向道路
                    {
                        if (state.rider_y[i] < state.rider_nexty[i])
                        {
                            state.rider_X[i] += 1;
                        }
                        else if (state.rider_y[i] > state.rider_nexty[i])
                        {
                            state.rider_X[i] -= 1;
                        }
                    }
                    else if (state.rider_x[i] % 2 == 1 && state.rider_y[i] % 2 == 1) //路口
                    {
                        if (state.rider_x[i] != state.rider_nextx[i])
                        {
                            if (state.rider_x[i] < state.rider_nextx[i])
                            {
                                state.rider_Y[i] += 1;
                            }
                            else if (state.rider_x[i] > state.rider_nextx[i])
                            {
                                state.rider_Y[i] -= 1;
                            }
                        }
                        else
                        {
                            if (state.rider_y[i] < state.rider_nexty[i])
                            {
                                state.rider_X[i] += 1;
                            }
                            else if (state.rider_y[i] > state.rider_nexty[i])
                            {
                                state.rider_X[i] -= 1;
                            }
                        }
                    }
                }
                putimage(state.rider_X[i], state.rider_Y[i], rider);
            }

            if (a == 1)
            {
                int tempx, tempy;
                //判断是否停靠
                for (int i = 0; i < rider_num; i++)
                {
                    if (message[i].finish != 0)
                    {
                        tempx = message[i].y1 / 2 * 80;
                        tempy = message[i].x1 / 2 * 80;
                        putimage(tempx + 5, tempy + 5, ok);
                    }
                    if (message[i].current != 0)
                    {
                        if (message[i].repeat == 0)
                        {
                            if (message[i].current == 1)
                            {
                                tempx = state.rider_y[i] / 2 * 80;
                                tempy = (state.rider_x[i] - 1) / 2 * 80;
                            }
                            else
                            {
                                tempx = state.rider_y[i] / 2 * 80;
                                tempy = (state.rider_x[i] + 1) / 2 * 80;
                            }
                            putimage(tempx + 5, tempy + 5, Love);
                        }
                        else
                        {
                            if (message[i].current == 1)
                            {
                                tempx = state.rider_y[i] / 2 * 80;
                                tempy = (state.rider_x[i] - 1) / 2 * 80;
                            }
                            else
                            {
                                tempx = state.rider_y[i] / 2 * 80;
                                tempy = (state.rider_x[i] + 1) / 2 * 80;
                            }
                            putimage(tempx + 5, tempy + 5, ok);
                        }
                    }
                }
            }

            //要使用特殊格式化字符请用outtextrect
            char text[100];
            setcolor(BLACK);
            if (automode)
                sprintf(text, "浩博制作\n\n时间:%d\n\n钱:%d\n\n接单数:%d\n\n完成数:%d\n\n超时数:%d\n\n骑手数量:%d\n\n从文件读取信息", cnt_t, MONEY, state.accept, state.finish, state.overtime, rider_num);
            else
                sprintf(text, "浩博制作\n\n时间:%d\n\n钱:%d\n\n接单数:%d\n\n完成数:%d\n\n超时数:%d\n\n骑手数量:%d\n\n鼠标拖拽房子派单", cnt_t, MONEY, state.accept, state.finish, state.overtime, rider_num);
            outtextrect(760, 160, 200, 400, text);
        }
        update_xy();
    }
}

void update_xy() //更新x和y的值
{
    for (int i = 0; i < rider_num; i++)
    {
        if (state.rider_x[i] % 2 == 0 && state.rider_y[i] % 2 == 1) //纵向道路
        {
            if (state.rider_x[i] < state.rider_nextx[i])
            {
                state.rider_x[i] += 1;
            }
            else if (state.rider_x[i] > state.rider_nextx[i])
            {
                state.rider_x[i] -= 1;
            }
        }
        else if (state.rider_x[i] % 2 == 1 && state.rider_y[i] % 2 == 0) //横向道路
        {
            if (state.rider_y[i] < state.rider_nexty[i])
            {
                state.rider_y[i] += 1;
            }
            else if (state.rider_y[i] > state.rider_nexty[i])
            {
                state.rider_y[i] -= 1;
            }
        }
        else if (state.rider_x[i] % 2 == 1 && state.rider_y[i] % 2 == 1) //路口
        {
            if (state.rider_x[i] != state.rider_nextx[i])
            {
                if (state.rider_x[i] < state.rider_nextx[i])
                {
                    state.rider_x[i] += 1;
                }
                else if (state.rider_x[i] > state.rider_nextx[i])
                {
                    state.rider_x[i] -= 1;
                }
            }
            else
            {
                if (state.rider_y[i] < state.rider_nexty[i])
                {
                    state.rider_y[i] += 1;
                }
                else if (state.rider_y[i] > state.rider_nexty[i])
                {
                    state.rider_y[i] -= 1;
                }
            }
        }
    }
}

和图形库有关的都在main.cpp内。其他代码先不上传了。

运行结果

通过文件读取订单信息

只要可执行文件同一文件夹内存在sales.txt,则自动为通过文件读取订单信息;否则就是手动通过鼠标拖拽派单。

通过鼠标拖拽派单

可执行程序

点此下载
解压后运行main.exe即可。
sales.txt
这里是一个样例sales.txt,大家可以下载。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部