所用工具:
vs 2019.
easyx 2021.
要用到的头文件有:
#include<stdio.h>
#include<graphics.h>
#include<time.h>
#include<graphics.h>
#include<mmsystem.h>//多媒体接口
#pragma comment(lib, "WINMM.LIB")
#include<windows.h>
#include<conio.h>
#include<stdlib.h>
#include<time.h>
//这两个是我自定义的头文件
#include "snake.h"
#include"food.h"
背景文本等,放在了background.cpp,要用的时候直接声明
//主菜单文本
void Text1(void)
{
settextstyle(30, 0, L"楷体");//设置文字格式
setbkmode(TRANSPARENT);// 去掉文字背景(透明)
outtextxy(343, 115, L"开始游戏");
outtextxy(343, 215, L"音乐效果");
outtextxy(360, 315, L"排行榜");
outtextxy(343, 415, L"退出游戏");
}
//点开始游戏文本
void Text2(void)
{
settextstyle(30, 0, L"楷体");//设置文字格式
setbkmode(TRANSPARENT);// 去掉文字背景(透明)
outtextxy(365, 115, L"简单");
outtextxy(365, 215, L"普通");
outtextxy(365, 315, L"困难");
outtextxy(335, 415, L"返回主菜单");
}
//排行榜文本
void Text3(void)
{
settextstyle(30, 0, L"楷体");//设置文字格式
setbkmode(TRANSPARENT);// 去掉文字背景(透明)
outtextxy(330, 115, L"简单分数榜");
outtextxy(330, 215, L"普通分数榜");
outtextxy(330, 315, L"困难分数榜");
outtextxy(330, 415, L"返回主菜单");
}
//音乐菜单文本
void Text4(void)
{
settextstyle(30, 0, L"楷体");//设置文字格式
setbkmode(TRANSPARENT);// 去掉文字背景(透明)
outtextxy(330, 115, L"开背景音乐");
outtextxy(330, 215, L"关背景音乐");
outtextxy(330, 315, L"返回主菜单");
}
//游戏结束页面文本
void Textebkg(void)
{
settextstyle(80, 0, L"楷体");//设置文字格式
setbkmode(TRANSPARENT);// 去掉文字背景
outtextxy(190, 50, L"历史最高分");
settextstyle(30, 0, L"楷体");
outtextxy(300, 300, L"点击返回菜单");
}
//菜单通用背景
void menubkg(void)
{
IMAGE img;//定义变量 存放图片
loadimage(&img, L"背景.jpg", 800, 480);//加载图片
initgraph(800, 480);//初始化一个800*480的窗口
putimage(0, 0, &img);//将图片作为背景
setfillcolor(LIGHTBLUE);//设置小方框的背景颜色(亮蓝)
fillrectangle(300, 100, 500, 150);
fillrectangle(300, 200, 500, 250);
fillrectangle(300, 300, 500, 350);
fillrectangle(300, 400, 500, 450);
}
//菜单二号通用背景
void menubkg1(void)
{
IMAGE img;//定义变量 存放图片
loadimage(&img, L"背景.jpg", 800, 480);//加载图片
initgraph(800, 480);//初始化一个800*480的窗口
putimage(0, 0, &img);//将图片作为背景
setfillcolor(LIGHTBLUE);//设置小方框的背景颜色(亮蓝)
fillrectangle(300, 100, 500, 150);
fillrectangle(300, 200, 500, 250);
fillrectangle(300, 300, 500, 350);
}
//游戏边界(墙壁)
void Gamewall(void)
{
setfillcolor(BROWN);//设置填充颜色
HRGN rgn4 = CreateRectRgn(10, 470, 590, 480);//创建一块矩形区域
setcliprgn(rgn4);//矩形区域剪裁
DeleteObject(rgn4);//不要占用系统内存
solidrectangle(10, 470, 590, 480);//实心的矩形区域
//上边的墙壁
HRGN rgn3 = CreateRectRgn(10, 0, 590, 10);//创建一块矩形区域
setcliprgn(rgn3);//矩形区域剪裁
DeleteObject(rgn3);//不要占用系统内存
solidrectangle(10, 0, 590, 10);//实心的矩形区域
//左边的墙壁
HRGN rgn2 = CreateRectRgn(0, 0, 10, 480);//创建一块矩形区域
setcliprgn(rgn2);//矩形区域剪裁
DeleteObject(rgn2);//不要占用系统内存
solidrectangle(0, 0, 10, 480);//实心的矩形区域
//右边的墙壁
HRGN rgn1 = CreateRectRgn(590, 0, 600, 480);//创建一块矩形区域
setcliprgn(rgn1);//矩形区域剪裁
DeleteObject(rgn1);//不要占用系统内存
solidrectangle(590, 0, 600, 480);//实心的矩形区域
}
//客户指导区
void Clientarea(void)
{
setfillcolor(YELLOW);//设置填充颜色
HRGN rgn = CreateRectRgn(600, 0, 800, 480);//创建一块矩形区域
setcliprgn(rgn);//矩形区域剪裁
DeleteObject(rgn);//不要占用系统内存
solidrectangle(600, 0, 800, 480);//实心的矩形区域
setcolor(RED);//设置颜色
settextstyle(25, 0, L"楷体");//设置字体
setbkmode(TRANSPARENT);//透明的背景风
outtextxy(650, 50, L"贪吃蛇");
settextstyle(15, 0, L"楷体");//设置字体
outtextxy(650, 150, L"[游戏说明]");
outtextxy(650, 250, L"方向键:w,a,s,d 控制方向");
}
//合法游戏范围背景
void Gamearea(void)
{
HRGN rgn = CreateRectRgn(10, 10, 590, 470);//创建一块矩形区域580*460 10的直径 58*46个格子
setcliprgn(rgn);//矩形区域剪裁
DeleteObject(rgn);//不要占用系统内存
solidrectangle(10, 10, 590, 470);//实心的矩形区域
setbkcolor(RGB(220, 220, 220));//背景色
clearcliprgn();//清屏
}
//游戏结束界面
void Gameoverbkg(void)
{
IMAGE img1;//定义变量 存放图片
loadimage(&img1, L"背景.jpg", 800, 480);//加载图片
initgraph(800, 480);
cleardevice();//刷屏
putimage(0, 0, &img1);//将图片作为背景
setcolor(BLACK);
settextstyle(100, 0, L"楷体");//设置文字格式
setbkmode(TRANSPARENT);// 去掉文字背景
outtextxy(200, 50, L"游戏结束");
settextstyle(30, 0, L"楷体");
setfillcolor(LIGHTBLUE);//设置小方框的背景颜色(亮蓝)
fillrectangle(270, 300, 505, 330);
outtextxy(270, 300, L"点击返回难度菜单");
}
void Rnakinglistbkg(void)
{
IMAGE img1;//定义变量 存放图片
loadimage(&img1, L"背景.jpg", 800, 480);//加载图片
initgraph(800, 480);
cleardevice();//刷屏
putimage(0, 0, &img1);//将图片作为背景
setcolor(BLACK);//字体颜色
setfillcolor(LIGHTBLUE);//设置小方框的背景颜色(亮蓝)
fillrectangle(270, 300, 505, 330);
}
首先,做一个贪吃蛇,或者说做一个游戏,肯定要有一个菜单,
那么如何做一个菜单,但菜单也是要分级的,所以先做一个主菜单
主菜单中我是包含了开始游戏、音乐效果、排行榜和退出游戏:
void meunbkg(void);
void Text1(void);
int main() {
remeun:;
menubkg();//菜单通用背景
Text1();//主菜单中的文本
ExMessage m;//定义一个消息变量
while (1)
{
int flag=0;
m = getmessage(EM_MOUSE);//获取鼠标消息
if (m.x >= 300 && m.x <= 500 && m.y >= 100 && m.y <= 150) //如果在指定区域
{
//点击后开始游戏,进入游戏菜单
if (m.message == WM_LBUTTONDOWN)
{
Gamemeun();
flag = 1;
}
}
else if (m.x >= 300 && m.x <= 500 && m.y >= 200 && m.y <= 250)
{
//点击后开始游戏,进入音乐菜单
if (m.message == WM_LBUTTONDOWN)
{
Musicaffection();
flag = 1;
}
}
if (m.x >= 300 && m.x <= 500 && m.y >= 300 && m.y <= 350)
{
//点击后开始游戏,进入排行榜
if (m.message == WM_LBUTTONDOWN)
{
Rankinglist();
flag = 1;
}
}
if (m.x >= 300 && m.x <= 500 && m.y >= 400 && m.y <= 450)
{
//点击了退出游戏
if (m.message == WM_LBUTTONDOWN)
{
break;//点击后结束循环,退出游戏
}
}
if (flag) //如果flag为真(为1)时执行goto语句,返回到循环开头
{
goto remeun;
}
}
closegraph();//清屏
}
先从简单的音乐写起,当然·,音乐效果肯定要有一个子菜单,并实现播放和关闭背景音乐的功能:
void menubkg1(void);
void Text4(void);
void Musicaffection()
{
remeun:;
menubkg1();//菜单通用背景2
Text4();//音乐文本
ExMessage m;//定义一个消息变量
while (1)
{
int flag = 0;
m = getmessage(EM_MOUSE);//获取鼠标消息
if (m.x >= 300 && m.x <= 500 && m.y >= 100 && m.y <= 150) //如果在指定区域
{
if (m.message == WM_LBUTTONDOWN)
{
//开音乐
mciSendString(L"open YELLOW.mp3 alias asd", NULL, 0, NULL);
//音乐循环播放
mciSendString(L"play asd repeat", NULL, 0, NULL);
flag = 1;
}
}
else if (m.x >= 300 && m.x <= 500 && m.y >= 200 && m.y <= 250)
{
if (m.message == WM_LBUTTONDOWN)
{
//关音乐
mciSendString(L"stop asd ", NULL, 0, NULL);
flag = 1;
}
}
if (m.x >= 300 && m.x <= 500 && m.y >= 300 && m.y <= 350)
{
if (m.message == WM_LBUTTONDOWN)
{
//跳出这个循环,直接回到主菜单
goto fmeun;
}
}
if (flag)
{
//当flag为真(为1)时,回到开头。
goto remeun;
}
}
closegraph();
fmeun:;
}
现在,开始做开始游戏了,如果不用分难度的话,点击开始游戏就直接玩🐍,但如果要难度设置的话,就又要做一个子菜单,而难度设置的话,我选择的是速度快慢。选择完难度后,开始玩🐍
void Text2(void);
void menubkg(void);
void Gamemeun(void)
{
remeun:;
menubkg();//菜单通用背景
Text2();//难度文本
ExMessage m;//定义一个消息变量
int flag = 0;
while (1)
{
m = getmessage(EM_MOUSE);//获取鼠标消息
if (m.x >= 300 && m.x <= 500 && m.y >= 100 && m.y <= 150) //如果在指定区域
{
if (m.message == WM_LBUTTONDOWN)
{
//点击后开始玩简单蛇(200是指速度)
Startgame(200);
flag = 1;
}
}
else if (m.x >= 300 && m.x <= 500 && m.y >= 200 && m.y <= 250)
{
if (m.message == WM_LBUTTONDOWN)
{
//点击后开始玩普通蛇(100是指速度)
Startgame(100);
flag = 1;
}
}
else if (m.x >= 300 && m.x <= 500 && m.y >= 300 && m.y <= 350)
{
if (m.message == WM_LBUTTONDOWN)
{
//点击后开始玩困难蛇(50是指速度)
Startgame(50);
flag = 1;
}
}
else if (m.x >= 300 && m.x <= 500 && m.y >= 400 && m.y <= 450)
{
//点击了退出游戏,结束循环,返回主菜单
if (m.message == WM_LBUTTONDOWN)
{
break;
}
}
if (flag )
{
//当flag为时,回到开始选择难度
goto remeun;
}
}
closegraph();
}
接下来,就是做蛇了,而又要用链表,单链表还是双向链表,我选择双向链表,因为他比较方便
先创建一个snake.h头文件,在头文件中写出有关蛇的结构体
#ifndef __snake_H__
#define __snake_H__
typedef struct Snake
{
int x;//x坐标
int y;//y坐标
struct Snake* last;//一个指向前一个蛇的结构体的指针
struct Snake* next;//一个指向下一个蛇的结构体的指针
}snake, * link;//*link表示后面声明指针类型的时候不用加*
#endif
先在有链表了,是时候初始化
link head;//头节点
link end;//尾节点
//初始化双向链表
void Snake_creat_list()
{
//为其分配动态内存
head = (link)malloc(sizeof(snake));
end = (link)malloc(sizeof(snake));
//头节点指向的前一个节点为空,下一个节点为尾节点
head->last = NULL;
head->next = end;
//尾节点指向的前一个节点尾头节点,下一个节点为空
end->last = head;
end->next = NULL;
}
//也就是说当前蛇没头没尾没身子
现在链表有了,是时候创建蛇头了
void creat_snake_head(link head0, link end0)
{
link bodyh;//定义了一个节点
bodyh = (link)malloc(sizeof(snake));//分配内存
//头->蛇头->尾->NULL
head0->next = bodyh;
end0->last = bodyh;
//尾->蛇头->头->NULL
bodyh->last = head0;
bodyh->next = end0;
//赋予蛇头坐标
bodyh->x = 15;
bodyh->y = 15;
}
有了蛇头,那么就该画出蛇了
void draw_snake_body(link head0, link end0)
{
link p;//辅助节点
p = head0->next;//指向蛇头
setlinecolor(RED);
setfillcolor(RED);
while (p != end0)//单该节点不为空时,遍历这个链表
{
//画一个半径为5的圆
fillcircle((p->x) * 10 - 5, (p->y) * 10 - 5, 5);//这样可以简化坐标的计算
p = p->next;
}
}
当你已经画出了蛇,那么接下了就应该想办法那他动起来
char key = 'd';
void snake_move(link head0, link end0)
{
link p, q;//定义两个辅助节点分别指向蛇头蛇尾
p = head0->next;
q = end0->last;
while (q->last != head0)//从后向前遍历,这就是双向链表相对于单链表的优势
{
q->x = q->last->x;
q->y = q->last->y;
q = q->last;
}
if (key == 'd')//向左移动时
{
p->x++;
}
if (key == 'a')//向右移动时
{
p->x--;
}
if (key == 'w')//向上移动时
{
p->y--;
}
if (key == 's')//向下移动时
{
p->y++;
}
p = p->next;
}
这还需要键盘来控制蛇的移动
//接受键盘信息
void check_key(char key_t)
{
if (key_t == '\0')
{
return;
}
//当蛇向前时,肯定不能向后跑啊,向左向右也是
else if ((key_t == 'a') && (key != 'd'))
{
key = key_t;
}
else if ((key_t == 'd') && (key != 'a'))
{
key = key_t;
}
else if ((key_t == 'w') && (key != 's'))
{
key = key_t;
}
else if ((key_t == 's') && (key != 'w'))
{
key = key_t;
}
else
{
return;
}
玩过easyx多知道,如果不通过某种手段,蛇的移动都会留下痕迹,原本我选择通过覆盖的方式来实现蛇的移动而不是用cleardevice(),目的是为了减缓闪屏,但后面我发现一个可以那他不闪屏的函数,我想了想,还是不改用cleardevice,太麻烦了,都打好了。
//隐藏蛇身
void hid_snake(link head0, link end0)
{
link p;
p = head0->next;
setlinecolor(RGB(220, 220, 220));
setfillcolor(RGB(220, 220, 220));
while (p != end0)
{
fillcircle((p->x) * 10 - 5, (p->y) * 10 - 5, 5);
p = p->next;
}
}
现在该弄食物了,我也是自定义看一个food.h的头文件
#ifndef __food_H__
#define __food_H__
struct Food {
int x;//食物坐标
int y;
int flag;//食物是否被吃
};
#endif
开始在游戏区中随机生成食物
struct Food food;
void creat_food(link head0, link end0)
{
do
{
srand((unsigned)time(NULL));
//在x:2~58,y:2~46范围内随机生成食物
food.x = rand() % 58 + 2;
food.y = rand() % 46 + 2;
//这个是判断食物是否生成在食物内部,是的话就重新生成食物
check_food_and_snake(head0, end0);
} while (!food.flag );
draw_food();//画出食物
}
//画出食物
void draw_food(void)
{
setlinecolor(BLUE);
setfillcolor(YELLOW);
fillcircle(((food.x * 10) - 5), ((food.y * 10) - 5), 5);
}
void check_food_and_snake(link head0, link end0)
{
link p;
p = head0->next;
while (p->next != end0)
{
if (((food.x) == (p->x)) && ((food.y) == (p->y)))//如果食物重合0
{
food.flag = 0;
}
p = p->next;
}
food.flag = 1;//否则为1
}
当蛇吃到食物时
//蛇吃到食物
int score=1;//分数,每吃到一个食物得一分
void Eatflag(link head0, link end0)
{
link p, body;
p = head0->next;
if ((p->x) == (food.x) && (p->y) == (food.y))
{
score++;//每吃到一个食物得一分
body = ((link)malloc(sizeof(snake)));
p = end0->last;
//刚开始的身体坐标,随便赋予一个值,反正后面都会赋予身子正确的值
body->x = 0;
body->y = 0;
p->next = body;
end0->last = body;
body->last = p;
body->next = end0;
creat_food(head, end);
food.flag = 1;
}
}
好,现在判断蛇是否撞墙或者撞到自己
//检查蛇头碰撞蛇身
int check_if_head_hit_body(link head0, link end0)
{
link p, q;
p = head0->next;
q = end0->last;
while (q->last != head0)
{
if (p->x == q->x && p->y == q->y)
{
return 1;//撞了,放回1
}
q = q->last;
}
return 0;//没撞,返回0;
}
//判断是否撞墙
int snake_if_hit_wall(link head0, link end0)
{
link p;
p = head0->next;
if (((p->x) > 59) || ((p->x) < 2) || ((p->y) > 47) || ((p->y) < 2))
{
return 0;//撞了返回0
}
return 1;//没撞,返回1
}
现在,这个游戏的基本功能已经实现,现在该玩🐍了
char key_temp = '\0';
void Startgame(int n)
{
//创建游戏背景
cleardevice();
Gamewall();
Clientarea();
Gamearea();
//创建游戏中的蛇,食物
Snake_creat_list();
creat_snake_head(head, end);
draw_snake_body(head, end);
creat_food(head, end);
//执行后,任何绘图操作都将暂时不输出到屏幕上,直到执行 FlushBatchDraw 或 EndBatchDraw 才
//将之前的绘图输出。
BeginBatchDraw();
while (1)
{
Eatflag(head, end);
check_food_and_snake(head, end);
draw_food();
Sleep(n);//蛇移动的速度
hid_snake(head, end);
if (_kbhit())//接受按键的信息
{
key_temp = _getch();
}
check_key(key_temp);
snake_move(head, end);
if (!(snake_if_hit_wall(head, end)) || check_if_head_hit_body(head, end))
//当撞墙或撞到自己,游戏结束
{
freelink(head, end);//释放链表空间
break;
}
draw_snake_body(head, end);
FlushBatchDraw();
}
EndBatchDraw();
Gameover(score, n);//执行游戏结束的画面
score = 0;
}
//释放链表空间
void freelink(link head0, link end0)
{
link p, q;
q = head0->next;
while (head0->next != NULL)
{
p = head0;
head0 = head0->next;
free(p);
}
free(end0);
}
执行Gameover函数
//游戏结算
void Gameover(int score,int n) {
Gameoverbkg();
//显示得分
TCHAR grade[1000] = {0};
swprintf_s(grade, _T("最终得分:%d"), score);
setbkmode(TRANSPARENT);
settextcolor(BLACK);
outtextxy(310, 200, grade);
//保存分数并与历史最高分比较
Save_score(score,n);
ExMessage m;
while (1)
{
m = getmessage(EM_MOUSE);//获取鼠标消息
if (m.x >= 270 && m.x <= 505 && m.y >= 300 && m.y <= 330) //如果在指定区域
{
if (m.message == WM_LBUTTONDOWN)
{
//返回难度菜单
break;
}
}
}
}
现在,该解决分数和排行榜了
//保存分数到临时文件all.txt中
void Save_new_score(int Score) {
FILE* fp;
errno_t err = fopen_s(&fp, "all.txt", "w");//打开,只能写
if (NULL != fp) {
fprintf(fp, "%d", Score);//写入分数
}
fclose(fp);//关闭
}
//读出临时文件all.txt中的文件
int Read_new_Score() {
int h = 0;
FILE* fp;
errno_t err = fopen_s(&fp, "all.txt", "r");//打开,只能读
if (NULL != fp)
{
fscanf_s(fp, "%d", &h);//将值放入h中
}
fclose(fp);//关闭
return h;//返回h
}
//困难榜输入与读出
void in_hard_data(int score)
{
FILE* fp;
errno_t err = fopen_s(&fp, "harddata.txt", "w");
if (NULL != fp) {
fprintf(fp, "%d", score);
}
fclose(fp);
}
int out_hard_data()
{
int h = 0;
FILE* fp;
errno_t err = fopen_s(&fp, "harddata.txt", "r");
if (NULL != fp) {
fscanf_s(fp, "%d", &h);
}
fclose(fp);
return h;
}
//普通榜输入与读出
void in_general_data(int score)
{
FILE* fp;
errno_t err = fopen_s(&fp, "generaldata.txt", "w");
if (NULL != fp) {
fprintf(fp, "%d", score);
}
fclose(fp);
}
int out_general_data()
{
int h = 0;
FILE* fp;
errno_t err = fopen_s(&fp, "generaldata.txt", "r");
if (NULL != fp) {
fscanf_s(fp, "%d", &h);
}
fclose(fp);
return h;
}
//简单榜输入与读出
void in_easy_data(int score)
{
FILE* fp;
errno_t err = fopen_s(&fp, "easydata.txt", "w");
if (NULL != fp) {
fprintf(fp, "%d", score);
}
fclose(fp);
}
int out_easy_data()
{
int h = 0;
FILE* fp;
errno_t err = fopen_s(&fp, "easydata.txt", "r");
if (NULL != fp) {
fscanf_s(fp, "%d", &h);
}
fclose(fp);
return h;
}
功能函数已经准备完成了
//分数进行保存比较
void Save_score(int score, int n)
{
Save_new_score(score);
int h=0, t=0;
t = Read_new_Score();
if (n == 50) //困难
{
h = out_hard_data();
if (t > h)
{
in_hard_data(t);
}
}
if (n == 100)//普通
{
h = out_general_data();
if (t > h)
{
in_general_data(t);
}
}
if (n == 200)//简单
{
h = out_easy_data();
if (t > h)
{
in_easy_data(t);
}
}
}
已经实现了分数的读和写了,现在时候搞排行榜了,排行榜分简单,普通,困难,也就是要一个子菜单
void Rankinglist(void)
{
remeun:;
menubkg();
Text3();
ExMessage m;//定义一个消息变量
while (1)
{
int flag = 0;
m = getmessage(EM_MOUSE);//获取鼠标消息
if (m.x >= 300 && m.x <= 500 && m.y >= 100 && m.y <= 150) //如果在指定区域
{
if (m.message == WM_LBUTTONDOWN)
{
Rankinglist1(200);
flag = 1;
}
}
else if (m.x >= 300 && m.x <= 500 && m.y >= 200 && m.y <= 250)
{
if (m.message == WM_LBUTTONDOWN)
{
Rankinglist1(100);
flag = 1;
}
}
if (m.x >= 300 && m.x <= 500 && m.y >= 300 && m.y <= 350)
{
//点击了排行榜
if (m.message == WM_LBUTTONDOWN)
{
Rankinglist1(50);
flag = 1;
}
}
if (m.x >= 300 && m.x <= 500 && m.y >= 400 && m.y <= 450)
{
//点击了返回主菜单
if (m.message == WM_LBUTTONDOWN)
{
goto cmeun;
}
}
if (flag)
{
goto remeun;
}
}
closegraph();
cmeun:;
}
void Rankinglist1(int n)
{
Rnakinglistbkg();
setfillcolor(LIGHTBLUE);//设置小方框的背景颜色(亮蓝)
fillrectangle(270, 300, 505, 330);
Textebkg();
TCHAR grade[1000] = { 0 };
int score = 0;
score = Read_score(n);//读取对应难度的最高分
swprintf_s(grade, _T("得分: % d"), score);
//setbkmode(TRANSPARENT);
settextcolor(BLACK);
settextstyle(70, 0, L"楷体");//设置文字格式
outtextxy(220, 150, grade);
ExMessage m;
while (1)
{
m = getmessage(EM_MOUSE);//获取鼠标消息
if (m.x >= 270 && m.x <= 505 && m.y >= 300 && m.y <= 330) //如果在指定区域
{
if (m.message == WM_LBUTTONDOWN)
{
break;//返回子菜单
}
}
}
}
好了,现在贪吃蛇就做完了,真的是累死我了,用的东西多差不多,就是繁琐,可以看一下结果
插入的背景图片自备。
如果有不对的地方,还望指教