《程序与算法》课程设计(论文)指导书[2022-12-30]
《程序与算法》课程设计(论文)指导书
一、设计目的
使学生具备理论联系实际的程序设计思想,掌握数据与结构中线性表和二叉树和图原理与算法知识,并能结合课题中游戏项目或者信息管理系统项目应用实际,灵活采用相关数据逻辑结构,选择适当算法完成项目功能;学会运用数据结构的理论知识分析解决软件开发中数据表示、数据结构运用的能力。
二、设计原始资料或素材
课程设计的材料由课程组老师根据教学大纲要求,通过网络、参考书等资料收集整理出六个项目,项目介绍描述见下面每个子项目介绍。六个项目都属于软件开发方向,因此相关原始资料主要是项目的任务书、C语言程序开发参考教材、数据结构与算法教材、实验指导书和网络上相关参考内容(自己收集)。
三、设计内容
本课程提供6个项目,学生2人1组,每组任选其一完成。6个项目题目分别为:
(1)俄罗斯方块游戏设计;
(2)贪吃蛇游戏设计;
(3)飞机大战游戏设计;
(4)奖学金统计系统;
(5)药品管理系统;
(6)道路选择系统。
源码
https://pan.baidu.com/s/1pq1Nwwo0hlc_J84F93HM4A?pwd=1111
项目一 俄罗斯方块游戏设计
一、实训任务要求:
1)游戏界面友好,按键操作提示清晰;
2)能够完成俄罗斯方块的左右和向下移动;
3)能够完成消行和计分操作;
4)游戏失败退出和按键暂停;
5)界面中显示游戏操作提示信息和统计信息;
6)游戏运行不能有明显bug;
二、实训步骤建议
(一)游戏原理分析与俄罗斯方块表示
1、 了解俄罗斯方块游戏的运行操作方法
2、 开发环境要求
VS2010以上版本或者Dev C++都可以。
3、 游戏运行原理
- 俄罗斯方块表示
static const uint16_t gs_uTetrisTable[7][4] =
{
{ 0x00F0U, 0x2222U, 0x00F0U, 0x2222U }, // I型
{ 0x0072U, 0x0262U, 0x0270U, 0x0232U }, // T型
{ 0x0223U, 0x0074U, 0x0622U, 0x0170U }, // L型
{ 0x0226U, 0x0470U, 0x0322U, 0x0071U }, // J型
{ 0x0063U, 0x0264U, 0x0063U, 0x0264U }, // Z型
{ 0x006CU, 0x0462U, 0x006CU, 0x0462U }, // S型
{ 0x0660U, 0x0660U, 0x0660U, 0x0660U } // O型
};
解释: L型 0x0223 0000 0010 0010 0011
0000
0010
0010
0011
2) 游戏池的表示
static const uint16_t gs_uInitialTetrisPool[28] =
{
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U,
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U,
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U,
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xFFFFU, 0xFFFFU
};
空游戏区表示
有方块的游戏区,例如
static const uint16_t gs_uInitialTetrisPool[28] =
{
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U,
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U,
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U,
0xC003U, 0xC003U, 0xCC03U, 0xFFDBU, 0xC3FFU, 0xFFFFU, 0xFFFFU
};
(3)俄罗斯方块移动原理
(4)碰撞原理
当方块运行到上图的第二个情况下,检测到方块矩阵和游戏池矩阵1的地方有重叠,就认为生碰撞,需要回到上一状态即图1情况。方块每动一步就需要检测是否碰撞。
(5)消行原理
检测到碰撞后,方块停留在上一状态,这时检测游戏池是否出现全1的行,如果有可以消除改行,而且根据消行的行数不同给出不同的分数。
(6)游戏结束判断
给出一个新块,检测到碰撞就游戏结束。
(7)游戏数据结构定义TetrisManager和控制数据定义TetrisControl
typedef struct TetrisManager // 这个结构体存储游戏相关数据
{
uint16_t pool[28]; // 游戏池
int8_t x; // 当前方块x坐标,此处坐标为方块左上角坐标
int8_t y; // 当前方块y坐标
int8_t type[3]; // 当前、下一个和下下一个方块类型
int8_t orientation[3]; // 当前、下一个和下下一个方块旋转状态
unsigned score; // 得分
unsigned erasedCount[4]; // 消行数 表示一次消掉 1行,2行,3行或者4行的次数
unsigned erasedTotal; // 消行总数
unsigned tetrisCount[7]; // 各方块数
unsigned tetrisTotal; // 方块总数
bool dead; // 挂
} TetrisManager;
typedef struct TetrisControl // 这个结构体存储控制相关数据
{
bool pause; // 暂停
bool clockwise; // 旋转方向:顺时针为true
int8_t direction; // 移动方向:0向左移动 1向右移动
// 游戏池内每格的颜色
// 由于此版本是彩色的,仅用游戏池数据无法存储颜色信息
// 当然,如果只实现单色版的,就没必要用这个数组了
int8_t color[28][16];
} TetrisControl;
4、 实训成果:程序代码基本框架
定义程序的框架和需要的功能函数。
示例代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <conio.h>
#include <windows.h>
// =============================================================================
// 7种方块的4旋转状态(4位为一行)
static const uint16_t gs_uTetrisTable[7][4] =
{
{ 0x00F0U, 0x2222U, 0x00F0U, 0x2222U }, // I型
{ 0x0072U, 0x0262U, 0x0270U, 0x0232U }, // T型
{ 0x0223U, 0x0074U, 0x0622U, 0x0170U }, // L型
{ 0x0226U, 0x0470U, 0x0322U, 0x0071U }, // J型
{ 0x0063U, 0x0264U, 0x0063U, 0x0264U }, // Z型
{ 0x006CU, 0x0462U, 0x006CU, 0x0462U }, // S型
{ 0x0660U, 0x0660U, 0x0660U, 0x0660U } // O型
};
// =============================================================================
// 初始状态的游戏池
// 每个元素表示游戏池的一行,下标大的是游戏池底部
// 两端各置2个1,底部2全置为1,便于进行碰撞检测
// 这样一来游戏池的宽度为12列
// 如果想要传统的10列,只需多填两个1即可(0xE007),当然显示相关部分也要随之改动
// 当某个元素为0xFFFFU时,说明该行已被填满
// 顶部4行用于给方块,不显示出来
// 再除去底部2行,显示出来的游戏池高度为22行
static const uint16_t gs_uInitialTetrisPool[28] =
{
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U,
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U,
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U,
0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xC003U, 0xFFFFU, 0xFFFFU
};
#define COL_BEGIN 2
#define COL_END 14
#define ROW_BEGIN 4
#define ROW_END 26
// =============================================================================
typedef struct TetrisManager // 这个结构体存储游戏相关数据
{
uint16_t pool[28]; // 游戏池
int8_t x; // 当前方块x坐标,此处坐标为方块左上角坐标
int8_t y; // 当前方块y坐标
int8_t type[3]; // 当前、下一个和下下一个方块类型
int8_t orientation[3]; // 当前、下一个和下下一个方块旋转状态
unsigned score; // 得分
unsigned erasedCount[4]; // 消行数 表示一次消掉 1行,2行,3行或者4行的次数
unsigned erasedTotal; // 消行总数
unsigned tetrisCount[7]; // 各方块数
unsigned tetrisTotal; // 方块总数
bool dead; // 挂
} TetrisManager;
// =============================================================================
typedef struct TetrisControl // 这个结构体存储控制相关数据
{
bool pause; // 暂停
bool clockwise; // 旋转方向:顺时针为true
int8_t direction; // 移动方向:0向左移动 1向右移动
// 游戏池内每格的颜色
// 由于此版本是彩色的,仅用游戏池数据无法存储颜色信息
// 当然,如果只实现单色版的,就没必要用这个数组了
int8_t color[28][16];
} TetrisControl;
HANDLE g_hConsoleOutput; // 控制台输出句柄
// =============================================================================
// 函数声明
// 如果使用全局变量方式实现,就没必要传参了
void initGame(TetrisManager *manager, TetrisControl *control); // 初始化游戏
void restartGame(TetrisManager *manager, TetrisControl *control); // 重新开始游戏
void giveTetris(TetrisManager *manager); // 给一个方块
bool checkCollision(const TetrisManager *manager); // 碰撞检测
void insertTetris(TetrisManager *manager); // 插入方块
void removeTetris(TetrisManager *manager); // 移除方块
void horzMoveTetris(TetrisManager *manager, TetrisControl *control); // 水平移动方块
void moveDownTetris(TetrisManager *manager, TetrisControl *control); // 向下移动方块
void rotateTetris(TetrisManager *manager, TetrisControl *control); // 旋转方块
void dropDownTetris(TetrisManager *manager, TetrisControl *control); // 方块直接落地
bool checkErasing(TetrisManager *manager, TetrisControl *control); // 消行检测
void keydownControl(TetrisManager *manager, TetrisControl *control, int key); // 键按下
void setPoolColor(const TetrisManager *manager, TetrisControl *control); // 设置颜色
void gotoxyWithFullwidth(short x, short y); // 以全角定位
void printPoolBorder(); // 显示游戏池边界
void printTetrisPool(const TetrisManager *manager, const TetrisControl *control); // 显示游戏池
void printCurrentTetris(const TetrisManager *manager, const TetrisControl *control); // 显示当前方块
void printNextTetris(const TetrisManager *manager); // 显示下一个和下下一个方块
void printScore(const TetrisManager *manager); // 显示得分信息
void runGame(TetrisManager *manager, TetrisControl *control); // 运行游戏
void printPrompting(); // 显示提示信息
bool ifPlayAgain(); // 再来一次
// =============================================================================
// 主函数
int main()
{
return 0;
}
(二)游戏显示控制
1.实训内容
(1)控制台显示控制函数API的学习
(2)游戏边界和游戏池显示控制
2.实训流程
1、 Console窗口控制API函数
(1) 光标位置、显示/隐藏控制
HANDLE g_hConsoleOutput; // 控制台输出句柄
g_hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
// 获取控制台输出句柄 GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值)
CONSOLE_CURSOR_INFO cursorInfo = { 1, FALSE }; // 光标信息 1表示光标大小 false 表示光标不可见
SetConsoleCursorInfo(g_hConsoleOutput, &cursorInfo); // 设置光标隐藏
SetConsoleTitleA("俄罗斯方块控制台版——By: XXX");//窗口标题
(2) 光标显示颜色控制
SetConsoleTextAttribute(g_hConsoleOutput, 0xF0);//是Windows系统中一个可以设置控制台窗口字体颜色和背景色的计算机函数。后面参数,填十六进制数字,前面的数字代表背景色,后面的代表前景色。
(3)光标定位
SetConsoleCursorPosition(g_hConsoleOutput, cd);//cd 表示光标的坐标位置
static COORD cd;
// 以全角定位,每个符号占2位
void gotoxyWithFullwidth(short x, short y)
{
static COORD cd;
cd.X = (short)(x << 1); //全角显示,x实际位置是x坐标值的2倍
cd.Y = y;
SetConsoleCursorPosition(g_hConsoleOutput, cd);
}
2、游戏边界和游戏池显示控制
(1)显示游戏池边界
Void printPoolBorder()
{
//设置颜色
//循环显示每一行,边界用 “■”表示
}
(2)显示游戏池
void printTetrisPool(const TetrisManager *manager, const TetrisControl *control)
{
}
3、测试
(1)实验一、API函数使用
g_hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursorInfo = { 1, false };
SetConsoleCursorInfo(g_hConsoleOutput, &cursorInfo); // 设置光标隐藏
SetConsoleTextAttribute(g_hConsoleOutput, 0xDF);
gotoxyWithFullwidth(5,5);
printf("王");
SetConsoleTitleA("俄罗斯方块控制台版——By: XXX");
上述代码实现上图显示效果
(2)实验二 在制定位置显示边界
显示游戏池边界和游戏池的内容(方块内容自己设定)
示例代码:
// 显示游戏池边界
void printPoolBorder()
{
int8_t y;
SetConsoleTextAttribute(g_hConsoleOutput, 0xF0);
for (y = ROW_BEGIN; y < ROW_END; ++y) // 不显示顶部4行和底部2行
{
gotoxyWithFullwidth(10, y);
printf("%2s", "");
gotoxyWithFullwidth(23, y);
printf("%2s", "");
}
gotoxyWithFullwidth(10, y ); // 底部边界
printf("%28s", "");
}
// 显示游戏池
// 定位到游戏池中的方格
#define gotoxyInPool(x, y) gotoxyWithFullwidth(x + 9, y)
void printTetrisPool(const TetrisManager *manager, const TetrisControl *control)
{
int8_t x, y;
for (y = ROW_BEGIN; y < ROW_END; ++y) // 不显示顶部4行和底部2行
{
gotoxyInPool(2, y); // 定点到游戏池中的方格
for (x = COL_BEGIN; x < COL_END; ++x) // 不显示左右边界
{
if ((manager->pool[y] >> x) & 1) // 游戏池该方格有方块
{
// 用相应颜色,显示一个实心方块
SetConsoleTextAttribute(g_hConsoleOutput, 0x0A);
printf("■");
}
else // 没有方块,显示空白
{
SetConsoleTextAttribute(g_hConsoleOutput, 0);
printf("%2s", "");
}
}
}
}
主函数处添加如下代码:
printPoolBorder();
memcpy(manager->pool, gs_uTestTetrisPool, sizeof(uint16_t[28]));
printTetrisPool(manager, control);
(三)俄罗斯方块移动控制
1.实训内容
(1)俄罗斯方块出现 giveTetris(TetrisManager *manager);
每一个俄罗斯方块从最上端出现,默认在最上方的四行中间位置出现。
(2)俄罗斯方块插入、移出游戏池
(3)俄罗斯方块向下,左右移动和旋转
2.实训流程
(1)俄罗斯方块出现
要求:给出上述出现的方块,编写giveTetris(TetrisManager *manager)
1、 插入方块和移除方块
方块改变位置时通过插入方块函数insertTetris(TetrisManager *manager)把方块数据放入游戏池。
方块改变位置时,需要把原位置的方块数据清除掉。removeTetris(TetrisManager *manager)
(2)方块向下、左右移动和旋转
通过键盘方向键控制方块移动,也可以通过时钟控制方块自动下移。
形成如下操作函数。
horzMoveTetris(TetrisManager *manager, TetrisControl *control)
moveDownTetris(TetrisManager *manager, TetrisControl *control)
rotateTetris(TetrisManager *manager, TetrisControl *control)
这些函数操作中需要注意什么事项?
(四)碰撞控制
1.实训内容
(1)碰撞原理及其函数实现方法
(2)碰撞函数的调用场景
(3)消行操作
2.碰撞原理及其函数实现方法
每个方块在移动的一个新的位置都需要检测是否发生了碰撞。检测原理,方块所在位置与游戏池是否有重合?有重合则为碰撞,方块恢复到上一状态,当前方块停止移动,准备下一方块。如果没有重合则无碰撞,继续移动操作。
bool checkCollision(const TetrisManager *manager)
{
// 当前方块
uint16_t tetris = gs_uTetrisTable[manager->type[0]][manager->orientation[0]];
uint16_t dest = 0;
// 获取当前方块在游戏池中的区域:
// 游戏池坐标x y处小方格信息,按低到高存放在16位无符号数中
dest |= (((manager->pool[manager->y + 0] >> manager->x) << 0x0) & 0x000F);
dest |= (((manager->pool[manager->y + 1] >> manager->x) << 0x4) & 0x00F0);
dest |= (((manager->pool[manager->y + 2] >> manager->x) << 0x8) & 0x0F00);
dest |= (((manager->pool[manager->y + 3] >> manager->x) << 0xC) & 0xF000);
// 若当前方块与目标区域存在重叠(碰撞),则位与的结果不为0
return ((dest & tetris) != 0);
}
3.碰撞函数的调用场景
上一项目中测试了移动控制方块操作,方块可以自由移动,但是无考虑碰撞,是有问题的。实际上方块到新的位置前必须检测碰撞,因此只要涉及到方块移动的都要先检测碰撞。
比如:
horzMoveTetris(TetrisManager *manager, TetrisControl *control)
moveDownTetris(TetrisManager *manager, TetrisControl *control)
rotateTetris(TetrisManager *manager, TetrisControl *control)
giveTetris(TetrisManager *manager);
4.消行操作
检测到碰撞后,就应该检测是否有全1的行,如果有需要消行,重新整理游戏池数据。
bool checkErasing(TetrisManager *manager, TetrisControl *control)
消行注意:单独1行、连续两行、3行和4行的计分不一样。
(五)整体运行控制
1.实训内容
(1)整体控制流程
(2)主函数main()调整
(3)游戏循环运行控制
2.整体控制流程
初始化->显示游戏界面->开始游戏->游戏结束->是否重来->退出
标红过程可以循环重复。
2、 main()
int main()
{
TetrisManager tetrisManager;
TetrisControl tetrisControl;
initGame(&tetrisManager, &tetrisControl); // 初始化游戏
do
{
printPrompting(); // 显示提示信息
printPoolBorder(); // 显示游戏池边界
runGame(&tetrisManager, &tetrisControl); // 运行游戏
if (ifPlayAgain()) // 再来一次
{
SetConsoleTextAttribute(g_hConsoleOutput, 0x7);
system("cls"); // 清屏
restartGame(&tetrisManager, &tetrisControl); // 重新开始游戏
}
else
{
break;
}
} while (1);
gotoxyWithFullwidth(0, 0);
CloseHandle(g_hConsoleOutput);
return 0;
}
- 游戏运行控制,通过时钟、键盘控制游戏
// 运行游戏
void runGame(TetrisManager *manager, TetrisControl *control)
{
clock_t clockLast, clockNow;
clockLast = clock(); // 计时
printTetrisPool(manager, control); // 显示游戏池
while (!manager->dead) // 没挂
{
while (_kbhit()) // 有键按下
{
keydownControl(manager, control, _getch()); // 处理按键
}
if (!control->pause) // 未暂停
{
clockNow = clock(); // 计时
// 两次记时的间隔超过0.45秒
if (clockNow - clockLast > 0.45F * CLOCKS_PER_SEC)
{
clockLast = clockNow;
keydownControl(manager, control, 80); // 方块往下移
}
}
}
}
(六)其他功能设计
1.实训内容
(1)游戏消行数据的显示
(2)下一游戏块显示和键盘操作提示
(3)游戏块颜色控制
2 游戏消行数据的显示
消行函数中添加消行数据的统计信息,通过printScore(const TetrisManager *manager)显示结果。
// 显示得分信息
void printScore(const TetrisManager *manager)
{
static const char *tetrisName = "ITLJZSO";
int8_t i;
SetConsoleTextAttribute(g_hConsoleOutput, 0xE);
gotoxyWithFullwidth(2, 2);
printf("■得分:%u", manager->score);
gotoxyWithFullwidth(1, 6);
printf("■消行总数:%u", manager->erasedTotal);
for (i = 0; i < 4; ++i)
{
gotoxyWithFullwidth(2, 8 + i);
printf("□消%d:%u", i + 1, manager->erasedCount[i]);
}
gotoxyWithFullwidth(1, 15);
printf("■方块总数:%u", manager->tetrisTotal);
for (i = 0; i < 7; ++i)
{
gotoxyWithFullwidth(2, 17 + i);
printf("□%c形:%u", tetrisName[i], manager->tetrisCount[i]);
}
}
3 下一游戏块显示
void printNextTetris(const TetrisManager *manager)
{
int8_t i;
uint16_t tetris;
// 边框
SetConsoleTextAttribute(g_hConsoleOutput, 0xF);
gotoxyWithFullwidth(26, 1);
printf("┏━━━━┳━━━━┓");
gotoxyWithFullwidth(26, 2);
printf("┃%8s┃%8s┃", "", "");
gotoxyWithFullwidth(26, 3);
printf("┃%8s┃%8s┃", "", "");
gotoxyWithFullwidth(26, 4);
printf("┃%8s┃%8s┃", "", "");
gotoxyWithFullwidth(26, 5);
printf("┃%8s┃%8s┃", "", "");
gotoxyWithFullwidth(26, 6);
printf("┗━━━━┻━━━━┛");
// 下一个,用相应颜色显示
tetris = gs_uTetrisTable[manager->type[1]][manager->orientation[1]];
SetConsoleTextAttribute(g_hConsoleOutput, manager->type[1] | 8);
for (i = 0; i < 16; ++i)
{
gotoxyWithFullwidth((i & 3) + 27, (i >> 2) + 2);
((tetris >> i) & 1) ? printf("■") : printf("%2s", "");
}
// 下下一个,不显示彩色
tetris = gs_uTetrisTable[manager->type[2]][manager->orientation[2]];
SetConsoleTextAttribute(g_hConsoleOutput, 8);
for (i = 0; i < 16; ++i)
{
gotoxyWithFullwidth((i & 3) + 32, (i >> 2) + 2);
((tetris >> i) & 1) ? printf("■") : printf("%2s", "");
}
}
// 显示提示信息
void printPrompting()
{
SetConsoleTextAttribute(g_hConsoleOutput, 0xB);
gotoxyWithFullwidth(26, 10);
printf("■控制:");
gotoxyWithFullwidth(27, 12);
printf("□向左移动:← A 4");
gotoxyWithFullwidth(27, 13);
printf("□向右移动:→ D 6");
gotoxyWithFullwidth(27, 14);
printf("□向下移动:↓ S 2");
gotoxyWithFullwidth(27, 15);
printf("□顺时针转:↑ W 8");
gotoxyWithFullwidth(27, 16);
printf("□逆时针转:0");
gotoxyWithFullwidth(27, 17);
printf("□直接落地:空格");
gotoxyWithFullwidth(27, 18);
printf("□暂停游戏:回车");
gotoxyWithFullwidth(25, 23);
printf("■By: XXXXXXXXX");
}
(七)优化调试
1实训内容
(1)根据游戏问题进行调试
1、根据游戏问题进行调试
对各自的程序存在的问题进行调试优化。
2、根据个人能力考虑改进游戏
比如:改变游戏池的大小,添加方块的类型,设置特殊方块等。
项目二 贪吃蛇游戏设计
一、实训任务要求
(1)游戏界面友好,按键操作提示清晰;
(2)贪吃蛇能够自动上下左右移动;
(3)贪吃蛇碰到中间散落方块,蛇身加长;
(4)每次游戏贪吃蛇初始位置和方块随机;
(5)蛇的运动速度可以调整;
(6)游戏运行不能有明显bug;
二、实训步骤提示
1 总体结构提示
程序关键在于表示蛇的图形及蛇的移动。用一个小矩形快表示蛇的一节身体,身体每长一节,增加一个矩形块,蛇头用俩节表示。移动时必须从蛇头开始,所以蛇不能向相反的方向移动,如果不按任意键,蛇自行在当前方向上前移,但按下有效方向键后,蛇头朝着该方向移动,一步移动一节身体,所以按下有效方向键后,先确定蛇头的位置,而后蛇的身体随蛇头移动,图形的实现是从蛇头新位置开始画出蛇,这时,由于未清屏的原因,原来的蛇的位置和新蛇的位置差一个单位,所以看起来蛇多一节身体,所以将蛇的最后一节用背景色覆盖。食物的出现与消失也是画矩形块和覆盖矩形块。
为了便于理解,定义两个结构体:食物与蛇。
表示食物和蛇的矩形都设计为10x10个像素单位,食物的基本数据域为它所出现的位置,用X和 y 坐标表示,则矩形块用函数rectangle(x, y,x+10,y+10)或rectangle(x, y, x+10,y-10)可以画出。由于每次只随机出现一个食物,而食物被吃掉后,才出现下一个食物,所以设定一个标志表示是否要出现一个食物的变量。
蛇的一节身体为一个矩形块,这样表示每个矩形块只需起点坐标x和y。身体是不断增长的,所以用数组存放每一节的坐标,最大设定为N=200, node表示当前节数。另外还需要保存蛇移动方向的变量position,若蛇头即将到达的位置是墙或者蛇身,则游戏结束。
(1)基本数据成员分析:蛇、食物、坐标的表示
结构体定义(比如:蛇)
struct mySnake //蛇
{
int num;
MYPOINT xy[MAX];
char postion; //表示方向,标记
}snake;
(2)过程分析模块:采用函数描述
①绘制游戏边框
②显示蛇:void DrawSnake(int flag);
③蛇身的移动:void MoveSnake(int x, int y);
④按键操作:void run(int x, int y);
⑤显示食物:void drawFood()
⑥玩游戏(吃食物):void Game();
⑦显示积分和游戏结束:void JudgeFunc(int x, int y);
(3)主函数调用模块main()
2. 初始化蛇和绘制蛇:
//1.初始化蛇void initSnake()
初始化蛇的坐标位置、方向、和节数。
//2.绘制蛇
void drawSnake()
{
for (int i = 0; i < snake.num; i++)
{
setlinecolor(RED);
setfillcolor(GREEN);
fillrectangle(snake.xy[i].x, snake.xy[i].y, snake.xy[i].x + 10, snake.xy[i].y + 10);
}
}
3.初始化食物和绘制食物的函数:
//1.初始化食物void initFood()
需要注意:食物不能出现在蛇身上; 食物的随机坐标产生只能是10的整数倍,蛇头才能对齐食物
//2.绘制食物
void drawFood()
{
fillrectangle(food.foodxy.x, food.foodxy.y, food.foodxy.x + 10, food.foodxy.y + 10);
}
4.移动蛇
注意:(1)蛇吃了食物后,除了第一节之外,后面的坐标都是前一节坐标;(2)蛇头怎么走,要根据方向标志去做移动,千万不要一下头朝前,一下尾巴朝前。
void moveSnake()
{
//除了第一节之外,后面的坐标都是前一节坐标
for (int i = snake.num - 1; i > 0; i--)
{
snake.xy[i].x = snake.xy[i - 1].x;
snake.xy[i].y = snake.xy[i - 1].y;
}
//蛇头怎么走,要根据方向标志去做移动
switch (snake.postion)
{
}
5.项目重点,如何去控制蛇,键盘控制按键
//把方向枚举处出来(小键盘,键码值)
enum movPosition{right=72,left=75,down=77,up=80};
5.蛇吃食物
蛇头坐标与食物坐标相等
void voidGame()
{
if (snake.xy[0].x == food.foodxy.x && snake.xy[0].y == food.foodxy.y)
{
snake.num++;
food.eatGrade += 10;
food.flag = 0;
}
}
要注意:蛇死亡判断,也就是游戏结束的判定。
蛇撞墙:蛇的坐标超出边框的坐标大小,游戏结束;
蛇撞自己:蛇头撞到蛇身的任意一节,游戏结束。
//撞自己
for (int i = 1; i < snake.num; i++)
{
if (snake.xy[0].x == snake.xy[i].x&&snake.xy[0].y == snake.xy[i].y)
{ MessageBox(hwnd, "游戏结束!", "撞自己!", 0);
return 1;
}
}
项目三 飞机大战游戏设计
一、实训任务要求
(1)游戏界面友好,按键操作提示清晰;
(2)能够完成飞机的左右和向下移动;
(3)能够完成子弹的发出和击中操作;
(4)能够完成血量和计分操作;
(5)游戏失败重新运行操作;
(6)游戏运行不能有明显bug;
二、实训步骤提示
(一)画出游戏主界面设计
键入最基本的图形界面代码查看效果,其中initgraph为初始化窗口
#include <stdio.h>
#include <graphics.h>
#include <conio.h>
int main()
{
initgraph(640, 480);
while(1){
}
return 0;
}
运行得到结果:
将图片包复制到当前文件夹中
在代码中声明一个背景图片变量,并设置函数添加一张背景图片,同时将窗口大小改为背景图片大小480*700,打开界面的指令航调试工具SHOWCONSOLE。上述步骤后代码更新为:
#include <stdio.h>
#include <graphics.h>
#include <conio.h>
//设置超参数
enum Hyperparameter{
WIDTH=480,
HIGHT=700
};
IMAGE bk; //背景图片变量
void loadImg(); //加载所有图片
void gameDraw(); //将所有图片贴到对应的位置
int main()
{
initgraph(WIDTH, HIGHT, SHOWCONSOLE); //初始化图形界面
loadImg(); //加载背景图
gameDraw(); //将所有图片贴到对应的位置
while(1){
}
return 0;
}
//加载所有图片
void loadImg(){
//加载背景图
loadimage(&bk, "./images/background.png");
}
//将所有图片贴到对应的位置
void gameDraw(){
//布置背景图
putimage(0,0,&bk);
}
运行得到界面:
(2)角色贴图
由于后续需要加载PNG透明图片,所以注册并添加透明贴图方法drawAlpha
注册代码:
// 载入PNG图并去除透明部分
void drawAlpha(IMAGE * picture, int picture_x, int picture_y); //x为载入图片的X坐标,y为Y坐标
实现代码(下列部分直接复制粘贴即可):
// 载入PNG图并去透明部分
void drawAlpha(IMAGE * picture, int picture_x, int picture_y) //x为载入图片的X坐标,y为Y坐标
{
// 变量初始化
DWORD* dst = GetImageBuffer(); // GetImageBuffer()函数,用于获取绘图设备的显存指针,EASYX自带
DWORD* draw = GetImageBuffer();
DWORD* src = GetImageBuffer(picture); //获取picture的显存指针
int picture_width = picture->getwidth(); //获取picture的宽度,EASYX自带
int picture_height = picture->getheight(); //获取picture的高度,EASYX自带
int graphWidth = getwidth(); //获取绘图区的宽度,EASYX自带
int graphHeight = getheight(); //获取绘图区的高度,EASYX自带
int dstX = 0; //在显存里像素的角标
// 实现透明贴图 公式: Cp=αp*FP+(1-αp)*BP , 贝叶斯定理来进行点颜色的概率计算
for (int iy = 0; iy < picture_height; iy++)
{
for (int ix = 0; ix < picture_width; ix++)
{
int srcX = ix + iy * picture_width; //在显存里像素的角标
int sa = ((src[srcX] & 0xff000000) >> 24); //0xAArrggbb;AA是透明度
int sr = ((src[srcX] & 0xff0000) >> 16); //获取RGB里的R
int sg = ((src[srcX] & 0xff00) >> 8); //G
int sb = src[srcX] & 0xff; //B
if (ix >= 0 && ix <= graphWidth && iy >= 0 && iy <= graphHeight && dstX <= graphWidth * graphHeight)
{
if ((ix + picture_x) >= 0 && (ix + picture_x) <= graphWidth) //防止出边界后循环显示
{
dstX = (ix + picture_x) + (iy + picture_y) * graphWidth; //在显存里像素的角标
int dr = ((dst[dstX] & 0xff0000) >> 16);
int dg = ((dst[dstX] & 0xff00) >> 8);
int db = dst[dstX] & 0xff;
draw[dstX] = ((sr * sa / 255 + dr * (255 - sa) / 255) << 16) //公式: Cp=αp*FP+(1-αp)*BP ; αp=sa/255 , FP=sr , BP=dr
| ((sg * sa / 255 + dg * (255 - sa) / 255) << 8) //αp=sa/255 , FP=sg , BP=dg
| (sb * sa / 255 + db * (255 - sa) / 255); //αp=sa/255 , FP=sb , BP=db
}
}
}
}
}
仿照背景图片的加载和粘贴,贴入角色图片
声明代码:
IMAGE imgRole;//角色图片数组
loadImg()中加入代码:
//加载角色图
loadimage(&imgRole, "./images/me1.png");
gameDraw()中加入代码:
drawAlpha(&imgRole, WIDTH/2-51, HIGHT-100); //布置角色图
编译运行得到:
(3)角色移动
当前角色的坐标是固定的,不能够变化,所以我们要将角色的一些属性封装成结构体,请添加定义代码:
//定义飞机结构体
struct Plance{
int x; //飞机横坐标
int y; //飞机纵坐标
bool live; //飞机是否存活
int width; //飞机图片宽度
int height; //飞机图片长度
int hp; //飞机血量
int type; //飞机类型
}player;
2.在游戏开始前,需要设置角色的位置,于是建立初始化函数gameInit(),在其中定义角色的位置,同时将加载背景图函数也放入初始化中执行,然后使用gameInit()方法在主函数中代替loadImg()方法
请添加函数定义代码:
// 初始化游戏函数
void gameInit(){
loadImg(); //加载背景图
player.x = WIDTH/2-51;
player.y = HIGHT-100;
player.live = true;
}
主函数更新为:
int main()
{
initgraph(WIDTH, HIGHT, SHOWCONSOLE); //初始化图形界面
gameInit();
gameDraw(); //将所有图片贴到对应的位置
while(1){
}
return 0;
}
3.当前代码中仅使用了一次贴图函数gameDraw(),想要进行游戏就需要时刻刷新界面,于是将gameDraw()函数放入while循环中,并进行刷新优化
主函数代码更新为:
int main()
{
initgraph(WIDTH, HIGHT, SHOWCONSOLE); //初始化图形界面
gameInit(); // 初始化游戏函数
BeginBatchDraw(); //双缓冲绘图开始
while(1){
gameDraw(); //将所有图片贴到对应的位置
FlushBatchDraw(); //刷新缓存位置
}
EndBatchDraw(); //双缓冲绘图结束
return 0;
}
(二)角色移动
1.开始设计角色的移动,设置函数playerMove(int speed),不断检测键盘是否有输入,以判断角色的移动位置,并在主函数的while循环中的gameDraw()之后,加入检测函数
添加代码:
// 判断角色移动
void playerMove(int speed){
// 使用windows函数获取键盘输入,比较流畅
if(GetAsyncKeyState(VK_UP)||GetAsyncKeyState('W')) {
player.y -= speed;
}
if(GetAsyncKeyState(VK_DOWN)||GetAsyncKeyState('S')) {
player.y += speed;
}
if(GetAsyncKeyState(VK_LEFT)||GetAsyncKeyState('A')) {
player.x -= speed;
}
if(GetAsyncKeyState(VK_RIGHT)||GetAsyncKeyState('D')) {
player.x += speed;
}
}
主函数代码更新为:
int main()
{
initgraph(WIDTH, HIGHT, SHOWCONSOLE); //初始化图形界面
gameInit(); // 初始化游戏函数
BeginBatchDraw(); //双缓冲绘图开始
while(1){
gameDraw(); //将所有图片贴到对应的位置
playerMove(2);
FlushBatchDraw(); //刷新缓存位置
}
EndBatchDraw(); //双缓冲绘图结束
return 0;
}
运行得到结果,发现已经可以通过上下左右或者wasd对于角色进行操控,但是存在有时候会飞到画框外面的问题:
2.由于有时候会飞到画框外面,所以在函数函数playerMove(int speed)中添加边界检测代码
函数playerMove(int speed)更新为:
// 判断角色移动
void playerMove(int speed){
// 使用windows函数获取键盘输入,比较流畅
if(GetAsyncKeyState(VK_UP)||GetAsyncKeyState('W')) {
if (player.y>0){
player.y -= speed;
}
}
if(GetAsyncKeyState(VK_DOWN)||GetAsyncKeyState('S')) {
if(player.y<(HIGHT-100)){
player.y += speed;
}
}
if(GetAsyncKeyState(VK_LEFT)||GetAsyncKeyState('A')) {
if(player.x+51>0){
player.x -= speed;
}
}
if(GetAsyncKeyState(VK_RIGHT)||GetAsyncKeyState('D')) {
if(player.x-51<(WIDTH-100)){
player.x += speed;
}
}
}
编译运行之后可以发现,不会出现飞出窗口的现象:
(三)子弹的设置和移动
1 设置子弹的相关属性
(1)仿照前章节中角色贴图的部分设计实现子弹的贴图
请添加定义代码:
IMAGE imgbull; //子弹图片
在loadImg()函数中添加代码:
//加载子弹图
loadimage(&imgbull, "./images/bullet1.png");
(2)由于子弹也需要坐标属性,所以套用飞机结构体,定义子弹为新数组变量bull[BULLLET_NUM],其中BULLLET_NUM为定义的超参数,设为15
超参数部分更新为:
//设置超参数
enum Hyperparameter{
WIDTH=480,
HIGHT=700,
BULLLET_NUM=15
};
定义新变量:
struct Plance bull[BULLLET_NUM];
3.在初始化模块gameInit()函数中增加初始化子弹属性的部分,将子弹的显示状态置为false,初始坐标0,0
gameInit()函数中增加代码:
for(int i = 0; i < BULLLET_NUM; i++){
bull[i].x = 0;
bull[i].y = 0;
bull[i].live = false;}
4.在动态绘制图片模块gameDraw()函数中增加绘画可显示子弹的部分
gameDraw()函数中增加代码:
//布置子弹图
for(int i = 0; i < BULLLET_NUM; i++){
if(bull[i].live){
drawAlpha(&imgbull[0], bull[i].x, bull[i].y);
}
}
2.设计子弹的移动
(1)目前,子弹已经在内存中加载,但是仍需要思考如何将子弹发出。那么传统的飞机大战游戏都是使用空格键发出子弹,故这里我们将这个操作拆分,其一是生成一个子弹,其二是保证子弹的正常运行,其三是检测空格按键,下面的叙述将按照这个顺序进行。
(2)生成一个子弹的操作可以封装成一个函数createBullet(),让子弹从当前角色的正中位置出现
增加代码:
//如果子弹没有,就创建子弹
void createBullet(){
for(int i = 0; i < BULLLET_NUM; i++){
if(!bull[i].live){
bull[i].x = player.x+51;
bull[i].y = player.y;
bull[i].live = true;
break;
}
}
}
(3)保证子弹的正常运行的操作可以封装成一个函数bullMove(int speed),让子弹在出现后以speed的速度向上飞去,触墙为止
增加代码:
//如果子弹没有,就创建子弹
void createBullet(){
for(int i = 0; i < BULLLET_NUM; i++){
if(!bull[i].live){
bull[i].x = player.x+51;
bull[i].y = player.y;
bull[i].live = true;
break;
}
}
}
在主函数while(1)中增加代码:
bullMove(2);
(4)检测空格按键的操作可以放在检测角色移动函数playerMove(int speed)中,和上下左右操作一起被检测
函数playerMove(int speed)中增加代码:
//当按下空格键并且两次间隔小于100毫秒的时候发射子弹
if(GetAsyncKeyState(VK_SPACE)){
//创建一个子弹
createBullet();
}
编译运行后按空格键可以看到效果,但是仍存在所有子弹一起被打出的情况:
(5)针对上述问题,添加一个计时器来对子弹出现的间隔进行限制
引入包:
#include <time.h>
添加计时器变量:
static DWORD t[10]; //添加计时器变量
添加计时器函数timeCheck(int ms, int id):
//定时器
bool timeCheck(int ms, int id){
if(clock()-t[id]>ms){
t[id] = clock();
return true;
}
return false;
}
将函数playerMove(int speed)中的空格按键的操作更新为:
//当按下空格键并且两次间隔小于100毫秒的时候发射子弹
if(GetAsyncKeyState(VK_SPACE) && timeCheck(200, 1)){
//创建一个子弹
createBullet();
}
编译运行后可以发现子弹正常化:
(四)敌机的设置和移动
1设置敌机的相关属性
(1)仿照前章节中子弹贴图的部分设计实现敌机的贴图
请添加定义代码:
IMAGE imgEnEmy[2];//敌机图片数组
在loadImg()函数中添加代码:
//加载敌机图
loadimage(&imgEnEmy[0], "./images/enemy1.png");
loadimage(&imgEnEmy[1], "./images/enemy2.png");
(2)由于敌机也需要坐标属性,所以套用飞机结构体,定义子弹为新数组变量enemy[ENEMY_NUM],其中ENEMY_NUM为定义的超参数,设为10,另外还定义两个区分大小敌机的超参数BIG、SMALL
超参数部分更新为:
//设置超参数
enum Hyperparameter{
WIDTH=480,
HIGHT=700,
BULLLET_NUM=15,
ENEMY_NUM=10,
SMALL,
BIG
};
定义新变量:
struct Plance enemy[ENEMY_NUM];
(3)在初始化模块gameInit()函数中增加初始化敌机属性的部分,将子弹的显示状态置为false,初始坐标0,0
gameInit()函数中增加代码:
for(int i = 0; i < BULLLET_NUM; i++){
bull[i].x = 0;
bull[i].y = 0;
bull[i].live = false;
}
(4)在动态绘制模块gameDraw()函数中增加绘画可显示敌机的部分,通过判断类型是大还是小,贴不一样的图
gameDraw()函数中增加代码:
//布置敌机图
for(int i = 0; i < ENEMY_NUM; i++){
if(enemy[i].live){
if(enemy[i].type == SMALL){
drawAlpha(&imgEnEmy[0], enemy[i].x, enemy[i].y);
}
else if(enemy[i].type == BIG){
drawAlpha(&imgEnEmy[1], enemy[i].x, enemy[i].y);
}
}
}
2设计敌机的移动
(1)目前,敌机已经在内存中加载,但是仍需要思考敌机如何移动。传统的飞机大战游戏是自动生成敌机,故这里我们将这个操作拆分,其一是生成敌机,其二是保证敌机的正常运行,其三是判断子弹和敌机的关系,下面的叙述将按照这个顺序进行。
(2)生成敌机的操作可以封装成两个函数,分别是初始化敌机属性的函数enemyHp(int i),和具体生成敌机的函数createEnemy(),将具体敌机的live属性设置为true
增加代码:
//敌机属性设置
void enemyHp(int i){
if(rand()%10==0){
enemy[i].type = BIG;
enemy[i].hp = 3;
enemy[i].width = 69;
enemy[i].height = 99;
}
else{
enemy[i].type = SMALL;
enemy[i].hp = 1;
enemy[i].width = 57;
enemy[i].height = 43;
}
}
//刷新敌机
void createEnemy(){
for(int i = 0; i < ENEMY_NUM; i++){
if(!enemy[i].live){
enemy[i].live = true;
enemy[i].x = rand()%(WIDTH-60);
enemy[i].y = 0;
enemyHp(i);
break;
}
}
}
(3)保证敌机的正常运行的操作可以封装成一个函数enemyMove(int speed),让子弹在出现后以speed的速度向下运行,触墙为止
增加代码:
//每秒刷新的敌机移动检测
void enemyMove(int speed){
for(int i = 0; i < ENEMY_NUM; i++){
if(enemy[i].live){
enemy[i].y += speed;
if(enemy[i].y >= HIGHT){
enemy[i].live = false;
}
}
}
}
在主函数while(1)中增加代码:
//敌机生成速度
if(timeCheck(300, 0)){
createEnemy();
}
//敌机行驶速度
if(timeCheck(20, 2)){
enemyMove(1);
}
编译运行后可以看到效果,但是敌机此时仍无法被击中:
(4)那么此时我们将判断子弹和敌机的关系的操作可以封装为函数playPlance()中,判断子弹是否击中敌机,不同类型的敌机掉血情况不同
增加代码:
//判断子弹和敌机的关系
void playPlance(){
for(int i = 0; i < ENEMY_NUM; i++){
if(!enemy[i].live)
continue;
for(int k = 0; k < BULLLET_NUM; k++){
if(!bull[k].live)
continue;
if(bull[k].x>enemy[i].x && bull[k].x<enemy[i].x+enemy[i].width
&& bull[k].y>enemy[i].y && bull[k].y<enemy[i].y+enemy[i].height){
bull[k].live = false;
enemy[i].hp--;
}
}
if(enemy[i].hp<=0){
enemy[i].live = false;
}
}
}
在主函数while(1)中增加代码:
playPlance()
编译运行后可以看到效果:
(六)计分和血量设置
1.设置分数和血量
(1)定义超参数,当前分值
请添加定义代码:
int score = 0; //当前分值
(2)在判断子弹和敌机的关系的函数playPlance()中的敌机死亡部分添加计分代码
请在函数playPlance()中添加代码:
if(enemy[i].type==BIG){score+=50;}
else if(enemy[i].type==SMALL){score+=10;}
(3)添加在指定的位置显示生命值和分值的函数
showLifeAndScore(int x, int y, int life, int score)
请添加代码:
//在指定的位置显示生命值
void showLifeAndScore(int x, int y, int life, int score)
{
TCHAR time_text[50];
sprintf(time_text, _T("Life:%d Score:%d"), life, score);
// settextcolor(RGB(0, 0, 0));
settextstyle(30, 0, _T("黑体")); //为了演示,显示fps字体大小不宜太大
outtextxy(10, 20, time_text);
// printf("%s", time_text);
}
编译运行可看到效果,但生命值还未设置:
(4)在游戏初始化函数gameInit()中添加生命相关设定,并进行当前角色和敌机碰撞的检测
请在函数gameInit()中添加定义代码:
player.width = 102;
player.height = 126;
player.live = true;
player.hp = 100;
设置当前角色和敌机碰撞的检测函数:
//检测一个点是否在一个区域内函数
bool pointin(int x1, int y1, int x2, int y2, int width, int height){
if(x1>x2 && x1<x2+width && y1>y2 && y1<y2+height){
return true;
}else{
return false;
}
}
//敌机和本机是否相撞检测函数
bool crashJudge(struct Plance e){
if(pointin(e.x, e.y, player.x, player.y, player.width, player.height)||
pointin(e.x+e.width, e.y, player.x, player.y, player.width, player.height)||
pointin(e.x, e.y+e.height, player.x, player.y, player.width, player.height)||
pointin(e.x+e.width, e.y+e.height, player.x, player.y, player.width, player.height)
){
return true;
} else{
return false;
}
}
//随时间判断敌机和本机是否相撞
void planceCrash(){
for(int i = 0; i < ENEMY_NUM; i++){
if(!enemy[i].live)
continue;
if(crashJudge(enemy[i])){
enemy[i].live = false;
if(enemy[i].type == BIG){
player.hp -= 30;
}else if(enemy[i].type == SMALL){
player.hp -= 10;
}
}
}
}
在主函数while(1)中增加代码:
planceCrash()
编译运行可看到效果:
2.添加起始页和结束页
(1)目前,游戏基础设置已经完成,但游戏需要开始和结束页面,我们分别来进行添加。
(2)起始页添加我们可以封装为一个函数showBeginPicture(),进行一些布局的同时,通过_getch()方法等待玩家按键进入
添加代码:
//显示开场界面
void showBeginPicture()
{//布置背景图
drawAlpha(&bk,0, 0);
setbkmode(TRANSPARENT);// 字体透明
settextcolor(BGR(0xFFEC8B));
settextstyle(80, 0, _T("微软雅黑"));
outtextxy(WIDTH / 2 - 100, 100, _T("飞机大战"));
settextstyle(40, 0, _T("黑体"));
settextcolor(0xFFA500);
outtextxy(WIDTH / 2 - 100, 280, _T("W、S、A、D 移动"));
outtextxy(WIDTH / 2 - 100, 340, _T("K 发射子弹"));
outtextxy(WIDTH / 2 - 100, 400, _T("按任意键继续"));
FlushBatchDraw();
_getch();
}
(3)终止页添加我们可以封装为一个函数showGameOver()
添加代码:
//结束界面
void showGameOver()
{
settextcolor(BGR(0xFFEC8B));
settextstyle(80, 0, _T("微软雅黑"));
TCHAR time_text[50];
sprintf(time_text, _T("分数:%d"), score);
outtextxy(WIDTH / 2 - 160, 280, time_text);
outtextxy(WIDTH / 2 - 160, 360, _T("GAME OVER"));
outtextxy(WIDTH / 2 - 160, 440, _T("按空格键继续"));
outtextxy(WIDTH / 2 - 160, 440, _T("按空格键继续"));
FlushBatchDraw();
while (' ' != _getch());//等待用户输入空格
score = 0;//重置分数
gameInit();//重置飞行器的属性
showBeginPicture();//返回开场界面重新开始
}
(4)在判断角色和敌机是否相撞的函数planceCrash()中,添加检测用户血量是否小于等于0 的判断,如果是则进入终止页面
在函数planceCrash()的for循环中最后中添加代码:
if(player.hp<=0){
player.live = false;
showGameOver();
}
(5)在主方法的初始化游戏函数gameInit()之前添加进入页面函数showBeginPicture()
showBeginPicture(); //初始化之前打开启动页
(6)编译运行即可看到最终效果:
项目四 奖学金系统设计
一、实训任务要求
(1)友好的输入、输出和操作显示界面;
(2)数据存储到相应的文件中,能读出和保存学生的信息;
(3)能够插入、删除和修改学生信息;
(4)统计奖学金信息,包括奖学金类型、金额和总计和名次;
(5)程序运行不能有明显bug;
二、项目要求介绍
某校的惯例是在每学期的期末考试之后发放奖学金。发放的奖学金共有五种,获取的条件各自不同:
(1)院士奖学金,每人8000元,期末平均成绩高于80分(>80),并且在本学期内发表1篇或1篇以上论文的学生均可获得;
(2)五四奖学金,每人4000元,期末平均成绩高于85分(>85),并且班级评议成绩高于80分(>80)的学生均可获得;
(3)成绩优秀奖,每人2000元,期末平均成绩高于90分(>90)的学生均可获得;
(4)西部奖学金,每人1000元,期末平均成绩高于85分(>85)的西部省份学生均可获得;
(5)班级贡献奖,每人850元,班级评议成绩高于80分(>80)的学生干部均可获得;
只要符合条件就可以得奖,每项奖学金的获奖人数没有限制,每名学生也可以同时获得多项奖学金。例如姚林的期末平均成绩是87分,班级评议成绩82分,同时他还是一位学生干部,那么他可以同时获得五四奖学金和班级贡献奖,奖金总数是4850元。
需要完成以下内容:
1.项目需求分析
2.概要设计和详细设计
3.源代码
4.程序演示
三、实训步骤提示
1 基本数据成员分析:学生、得分的表示
结构体定义(比如:蛇)
struct myStudents //学生
{
int final_exam; //学生的期末平均成绩
char west; //西部学生
}students;
2 过程分析模块:采用函数描述
①输入学生的分数等信息
②获取输入的参数:void InfoInput();
③对奖学金进行排序:void SortScore(int x[]);
④计算奖学金:int calculate(myStudents student);
⑤输出奖学金:void Output(myStudents student);
3 主函数调用模块main()
4.计算奖学金:
int calculate(myStudents student)
{
// 根据学生的各项属性进行奖学金额的累计
// 设定一个变量用于保存奖学金总额
int scholarship = 0;
// 获取学生信息
int score = student.final_exam; // 获取期末平均分
int west = student.west; //获取是否西部学生
// 判断期末分数是否大于80
if( score > 85)
{
// 判断是否为西部学生
if(west==’Y’)
{
scholarship += 1000; // 获得1000元西部奖学金
}
if(…) // 判断是否获得五四奖学金
{…}
…
}
return scholarship;
项目五 药品管理系统
一、实训任务要求
(1)友好的输入、输出和操作显示界面;
(2)信息保存到相应的文件中,能读出和保存药品信息;
(3)能够插入、删除和修改药品成绩;
(4)可统计药品失效日期、每日用量,按类别显示药品信息。
(5)程序运行不能有明显bug;
二、项目介绍
编写C语言程序,使用链表实现药品管理信息系统,并至少能够管理30种药品的相关信息。其中,药品信息主要包括药品名称、编号(12位的标识符)、生产日期(年月日)、有效期(月数)、失效日期(年月日)、主治病症类别(限定为感冒药、胃药、消炎药、滴眼液4类)、用法与用量。生产日期、有效期和失效日期均为非必要项,但需要满足合理条件,如表1所示。用法与用量请自行设计合理的结构体类型,能够存储至少4种不同的药品用法与用量说明,如“口服,一日3次,一次5片”、“外用,一日35次,一次12滴”、“饭后服用,一日23次,一次35mg,一日最多10mg”。
表1 药品相关信息合理条件表
可能
情况 生产日期 有效期 失效日期 合理与否 原因
20220601 24个月 20240601
1 有 有 有 合理与否取决于3个数据是否一致 /
2 有 有 / 合理 /
3 有 / 有 合理 /
4 有 / / 不合理 信息缺失
5 / 有 有 合理与否自行确定
6 / 有 / 不合理 信息缺失
7 / / 有 合理
8 / / / 不合理 信息缺失
具体功能要求如下:
(1)增加。能够从文件中录入多种药品的相关信息(全部信息或部分信息),也能够随时录入一种新药品的相关信息(全部信息或部分信息)。注意:需要考虑各种类型的不规范、不合理或错误数据,如编号位数不对、编号不唯一、日期格式不对、有效期非整数、三个日期相关数据不满足表1的条件等。
(2)修改。能够随时修改一种药品的相关信息,包括对已录入的信息进行修改或删除、对未录入的信息进行添加。
(3)删除。能够随时删除一种药品的所有信息。
(4)计算1。能够计算某种药品(按照编号或名称检索)的当前失效日期。
(5)计算2。能够计算某种药品(按照编号或名称检索)的一日用量。
(6)某种药品信息。能够打印某种药品(按照编号或名称检索)的所有信息。
(7)某类主治病症类别药品信息。能够按照编号顺序打印所有某类主治病症类别的药品信息。
(8)全部信息。能够按照编号顺序打印系统中的所胡药品信息。
(9)存储。能够将当前系统中的所有信息保存到文件中。
(10)过期药品信息。能够打印所有过期药品清单。
(11)即将过期药品信息。能够按照设定日期打印即将过期药品清单。
(12)其他你认为有用的附加功能,可酌情添加。
三、实训步骤提示
根据数据结构与算法教材内容,充分发挥个人主观能动性完成任务。
项目六 道路选择系统
一、实训任务要求
(1)友好的输入、输出和操作显示界面;
(2)至少输入8个城市的道路互联信息,文件形式存储;
(3)能够添加和修改和删除道路信息;
(4)能够添加城市和道路;
(5)根据某个城市到某个或者所有城市的最短道路。
(6)任一输入两个城市,要求求出他们之间的最短路径。
(7)程序运行不能有明显bug;
二、项目介绍
在交通网络非常发达的今天,人们出差、旅游或做其他出行时,不仅关心节省交通费用,而且对里程和所需时间等问题也很感兴趣。对于这样一个人们关心的问题,可用一个图结构来表示交通网络系统,利用计算机建立一个交通咨询系统。图中顶点表示城市,边表示城市之间的交通关系。设计一个交通咨询系统,能让旅客咨询从任一个城市顶点到达另外一个城市顶点之间的最短路径(里程)的问题。
(1)根据实际情况,先建立交通网络图的存储结构。
(2)求某个城市到达其余各城市的最短路径。
(3)任一输入两个城市,要求求出他们之间的最短路径。
要求:
根据下图一个简单的交通网络图(单位:km)
- 求顶点1(北京)到其余顶点的最短路径:
- 分别求顶点5(成都)到顶点7(上海)之间的最短路径、
- 顶点7(上海)到顶点2(西安)之间的最短路径。
三、实训步骤提示
最短路径的提法很多,在这里先讨论单源最短路径问题:即已知有向图(带权),我们希望找出从某个源点SEV到G中其余各顶点的最短路径。
为了叙述方便,我们把路径上的开始点称为源点,路径的最后一个顶点为终点。
那么如何求得给定有向图的单源最短路径呢?迪杰斯特拉(Dijkstra)提出按路径长度递增产生诸点的最短路径算法,称之为迪杰斯特拉算法。
迪杰斯特拉算法求最短路径的实现思想是:设G=(V.E)是一个有向图,结点集为,V={v1,v2,…,vn},cost是表示G的邻接矩阵,cost[i][j]表示有向边<i,j>的权。若不存在有向边<i,j>,则cost[i][j]的权为无穷大(这里取值为32767)。设S是一个集合,其中的每个元素表示一个顶点,从源点到这些顶点的最短距离已经求出。设顶点v1为源点,集合S的初态只包含一个元素,即顶点v1。数组dist 记录从源点到其他顶点当前的最短距离,其初值为dist[i]=cost[v1][i], i=1,2,…,n.从S之外的顶点集合V-S中选出一个顶点w.使dist[w]的值最小。于是从源点到达w只通过S中顶点,把加入集合S中,调整dist中记录的从源点到V-S中每个顶点v的距离从原来的dist[v]和dist[w]+cost[w][v]中选择较小的值作为新的dist[v]。重复上述过程,直到V-S为空。
最终结果是:S记录了从源点到该顶点存在最短路径的顶点集合,数组dist记录了源点到V中其余各顶点之间的最短路径,path是最短路径的路径数组,其中path[i]表示从源点到顶点i之间的最短路径的前驱顶点。
因此,迪杰斯特拉算法可用自然语言描述如下:
狄克斯特拉算法实例介绍
狄克斯特拉算法对求一个顶到到其他所有顶点的路径较优。初始时,S中只包含原点,顶点v到自己的距离为0,D中包含出v外的其他顶点,v到D中顶点u的距离为边上的权值。从D中选取一个顶点k,顶点v到顶点k的距离最小,然后把顶点k加入到S中,该选定的距离就是v到k的最短路径长度。以顶点k为新考虑的中间点,修改顶点v到U中各顶点的距离,若从原点v到顶点u的距离比原来的距离(不经过k)的距离看,短,则修改顶点u的距离值,修改后的距离值为顶点v到顶点k的距离加上边<k,u>上的权。
狄克斯特拉的算法:
D2[v1]=0;S[v1]=1;
for(i=2;i<n;i++)
{
min=IDF;
for(w=1;w<=n;w++)
if(!S[w]&&D2[w]<min)
{
v=w;min=D2[w];
}
S[v]=1;
for(w=1;w<=n;w++)
if(!S[w]&&(D2[v]+G->arcs[v][w]<D2[w]))
{
D2[w]=D2[v]+G->arcs[v][w];
P2[w]=v;
}
}
(三)任意一对顶点间最短路径
任意一对顶点间最短路径问题,是对于给定的有向网络图G=(V.E),要对G中任意一对顶点有序对“v,w(v≠w)”,找出v到w的最短路径。
要解决这个问题,我们可以依次把有向网络图中每个顶点作为源点,重复执行前面讨论的迪杰斯特拉算法n次,即可以求得每对顶点之间的最短路径。
这里还可以用另外一种方法,称作费洛伊德(Floyd)算法。
费洛伊德(Floyd)算法算法的基本思想是:假设求从顶点vi到vj的最短路径。如果从vi到vj存在一条长度为ares[i][j]的路径,该路径不一定是最短路径,还需要进行n次试探。首先考虑路径<vi,v1>和<v1,Vj>是否存在。如果存在,则比较<vi.vj>和<vi,v1,vj>的路径长度,取长度较短者为当前所求得的最短路径。该路径是中间顶点序号不大于1的最短路径。其次,考虑从vi到vj是否包含有顶点v2为中间顶点的路径<vi,…,v2,…,vj>,若没有,则说明从vi到vj的当前最短路径就是前一步求出的若有,那么<vi,…,v2,…vj>可分解为<vi,…,v2>和<v2,…,vj>,而这两条路径是前一次找到的中间顶点序号不大于1的最短路径,将这两条路径长度相加就得到路径<vi,…,v2,…,vj>的长度。将该长度与前一次中求出的从vi到vj的中间顶点序号不大于1的最短路径比较,取其长度较短者作为当前求得的从vi到vj的中间顶点序号不大于2的最短路径。依此类推,直到顶点v,加入当前从vi到vj的最短路径后,选出从v,到vj的中间项点序号不大于n的最短路径为止。由于图G中顶点序号不大于n,所以vi到vj的中间项点序号不大于n的最短路径,已考虑了所有顶点作为中间顶点的可能性,因此,它就是vi到vj的最短路径。
费洛伊德Floyd算法实例介绍
弗洛伊德算法对求任意两个顶点之间的路径较优。用邻接矩阵保存图存储后,另外需要存一个二维数组A存放当前顶点之间的最短路径长度。分量A[i][j]表示当前顶点i到j的最短路径长度。弗洛伊德算法的基本思维是递推产生一个矩阵序列A0,A1,A2,….Ak,… An,其中Ak[i][j]表示从顶点到vi到顶点vj的路径上所经过的顶点编号不大于k的最短路径长度。
A[i][j]=cost[i][j]
A(k+1)[i][j]=min{Ak[i][j],Ak[i+1][k+1]+Ak[k+1][j]}
弗洛伊德主要算法,若Ak[i][j]已求出,顶点i到顶点k+1的路径长度为Ak[i][k+1],顶点路径长度为Ak[i][j],顶点k+1到顶点j的路径长度为Ak[k+1][j],如果此时Ak[i][k+1]+Ak[k+1][j]<Ak[i][j],则将原来的顶点i到顶点j的路径改为顶点,否则不需要修改顶点i到j的路径。
for(k=1;k<=n;k++)
{
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
{ if(D[i][k]+D[k][j]<D[i][j])
{
D[i][j]=D[i][k]+D[k][j];
P[i][j]=P[i][k];
}
}}
七、所需仪器设备
个人计算机1台、Windows操作系统、C++开发环境(VS2010以上版本、Dev C++等)。
八、参考资料
[1] 陈媛.算法与数据结构[M].北京:清华大学出版社. 2020
[2] K. N. King. C语言程序设计[M].北京:清华人民邮电出版社.2010
[3] 童晶、丁海军、金永霞、周小芹. C语言课程设计与游戏开发实践 教程[M]. 北京:清华大学出版社.2017.8
[4] https://www.csdn.net/