魔塔是一款经典的策略类游戏。本文将基于C++与EasyX图形库,实现魔塔的基础改编版,相较于原版,增加了升级、血量上限等功能,设计了四层地图与出乎意料的结局,并合理安排数值,使游戏趣味性与挑战性兼备。
一点小小的建议:如果想要自己独自实现魔塔,可以先实现控制台版本,再图形化、插入音乐。文章难免有疏漏失误,还请读者指出。
本文重点不在于详细介绍实现魔塔的各种技术细节,想要根据本文实现自己的魔塔,必然需要进一步查阅相关知识,比如头文件的引用与函数声明,防止控制台弹出,EasyX各种库函数的使用,mciSendString的运用等。
魔塔基础改编版展示
1.下载EasyX。这是一个基于C++的图形库,针对Windows平台,易于上手。
2.图片素材准备。在网上搜索相应资源,将需要的图片依次截取下来并保存,注意素材最好用数字编号,便于后期图形化加载图片,以下是我图片的编号。
3.音频准备。请读者选择自己喜欢的音效与背景音乐,我的魔塔音效全部来源于塞尔达。下面介绍几个有用的关于音频处理的免费网站
在线转换MP3文件(无限制!) - ezyZip,可以实现多种文件格式的转换
Online MP3 Cutter - Cut Songs, Make Ringtones,剪切音频、设置淡入淡出等
在线音频剪辑丨剪切丨编辑-100Audio音频工具站
游戏实现的核心在于读取玩家操作并进行相应处理。player与monster可以用class创建,地图采用三维数组,每次有效操作后函数都会返回一个值,根据值判断游戏是否结束
创立多个源文件,分别对应main body, functions, audio, visualization, data,创建头文件联系各个源文件。这样做的好处在于使每个源文件都对应相应功能,实现模块化,便于管理
游戏核心代码完成后再增加图鉴、npc对话等拓展功能。
#pragma once
#undef UNICODE //这个东西非常重要,不加的话easyX中
//关于字符串的几乎都要报错,然后超级麻烦
#define _CRT_NONSTDC_NO_DEPRECATE
#define _CRT_SECURE_NO_WARNINGS
#define ROW 13
#define COL 13
#define FLOOR 4
#include
#include
#include
#include
#include
#pragma comment(lib,"winmm.lib")
#include
#include
#include
//map
extern int Map[FLOOR][ROW][COL];
//类
class Player {
public:
// 构造函数
Player();
int position[3]; // 位置数组
int hp; // 生命值
int hp_limit; // 生命值上限
int atk; // 攻击力
int def; // 防御力
int money; // 金钱
int Lv; // 等级
int Exp; // 经验值
int yellow_key; // 黄钥匙
int blue_key; // 蓝钥匙
int red_key; // 红钥匙
int pickaxe; //镐子
};
class Monster {
public:
Monster(int hp, int atk, int def, int exp,int gold/*, char* name*/);
int hp; // 生命值
int atk; // 攻击力
int def; // 防御力
int exp; // 经验值
int money;
};
//对象
extern Monster GreenSlime; extern Monster RedSlime;
extern Monster Skeleton; extern Monster SkeletonSoldier;
extern Monster Bat;
extern Monster Wizard;
extern Monster DemonKing;
extern Monster DemonEmperor;
//函数
//void display_map(int map[][COL]); //控制台版本
int play(int map[][COL], Player &p);
void ChangeFloor(int Map[FLOOR][ROW][COL], Player& P, int ret);
int Damage(Player& p, const Monster& m);
void LoadImages();
void StartImage();
void InventInfor();
void InitImage();
void MonsterEncyclopedia(Player& p);
void PrintMap(int map[][COL], Player& p); //图形化
void PrintInfor(char* name, int d_hp, int money, int exp);
void PrintInfor(char* name, int addHP);
void PrintMessage(const char* message);
void PrintNoKey();
void PrintLvUp();
void PrintShield(char* name, int defence);
void PrintSword(char* name, int attack);
void PrintLifeGem(int defence);
void PrintAttackGem(int attack);
void PrintSpecialInfor(const char* name);
void PrintFalsePrincess();
void PrintLose();
void PrintWin();
void exit();
void PrintUsePickaxe();
//audio
void musicGameLoading();
void musicPickup();
void musicBattle();
void musicEnter();
void musicLvUp();
void musicMeet();
void closeMusic();
void musicDie();
void musicWin();
void musicEncyclopedia();
顾名思义,此文件负责游戏主体逻辑,而不负责具体实现
技术细节
1. 地图所用的三维数组第三个维度表示楼层
2.Playsound函数只能播放wav格式的音频,且无法同时播放多个音频,所以此处用于循坏播放背景音乐。audio源文件使用mciSendString来实现播放多个音频。
#include"head.hpp"
int main()
{
Player P; //初始化
initgraph(850, 650, EX_NOCLOSE); // 初始化一个窗口
LoadImages(); // 加载图片素材
StartImage(); // 游戏开始画面
InventInfor(); //制作者信息
背景音乐
PlaySound(TEXT("ZeldaBackground.wav"), 0, SND_FILENAME | SND_ASYNC | SND_LOOP); //这个函数只适用于处理单个音频!
InitImage(); // 打印游戏过程中始终不变的图片
int ret;
int f = P.position[0];
while(P.position[0]
使用上下左右以及WSAD控制角色移动,GetAsyncKeyState读取玩家操作,至于如何实现一键一动,请参考GetAsyncKeyState函数实现一键一动-CSDN博客
用玩家试图移动到的位置对应地图上的值进行判断。比如遇到monster,则调用Battle函数;遇到门要判断有无相应颜色的钥匙,打开要消耗一把钥匙。有效操作后要把相应位置换为空地,所有无效操作后比如撞墙,都要把玩家坐标重置回操作前的坐标。
采用回合制,玩家先攻击,然后怪物,直至一方生命值变为0。造成的伤害为 己方攻击 - 对方防御。杀死怪物后会获得经验、金币
防御宝石(红)、攻击宝石(蓝),剑,盾,可以增加玩家属性。经验值大于当前等级*50即可升级,升级会消耗经验增加生命上限及攻击和防御。两种恢复药水分别加20和50的血,但血量不能超过上限,溢出的会被浪费。钥匙分为黄钥匙和蓝钥匙。
上下楼梯需判断玩家试图走的位置有无障碍物,如果有,则玩家从其他方向出来
只能用ESC键退出游戏
与贤者(Saint)、女神、或公主的对话,镐子,怪物之书(可计算怪物对玩家造成的伤害)。此处隐藏着我设计的几个彩蛋:与贤者的对话中有着结局的伏笔与游戏的部分攻略;向女神供奉需要金币,并且第一次供奉没有任何奖励;第三层的公主是假的,必须在遇到她后通过再次向女神供奉才能打开通往第四层的楼梯,从而拯救真正的公主;镐子前面不能用,要救出公主必须要用镐子
#include"head.hpp"
int OfferMoneyTimes = 0;//供奉给女神像的次数
int meetSage = 0;
int meetPrincess = 0;
//撞墙与否
int judge_hit(int value)
{
if (value == 0 || value == 1 || value == 52 || value == 53)
{
return 0;
}
return 1;
}
//没撞墙时
// 设置新坐标
void set_new_position(int position[], int r, int c)
{
position[1] = r;
position[2] = c;
}
//对战
void PK(Player& p, const Monster& m)
{
p.hp -= Damage(p, m);
}
//计算怪物对玩家伤害
int Damage(Player& p, const Monster& m)
{
int hp = p.hp;
int l = m.hp;
if (p.atk - m.def >= l) //一击毙命
return 0;
while (hp > 0 && l > 0)
{
if (p.def < m.atk)
hp -= m.atk - p.def;
if (m.def < p.atk)
l -= p.atk - m.def;
}
if (hp <= 0)
return p.hp; //死亡
else
return p.hp - hp;
}
//升级
//经验要求暂定为,50*Lv; 注意连续升级的情况!
//提升:血量加 100, 攻击 加5,防御加 5
void LvUp(Player& p)
{
p.hp_limit += 50;
p.atk += 2;
p.def += 2;
p.Lv += 1;
musicLvUp();
PrintLvUp();
}
void Judge_and_LvUp(Player& p)
{
while (p.Exp >= p.Lv * 50)
{
p.Exp -= p.Lv * 50;
LvUp(p);
}
}
//战斗
int Battle(int map[][COL], Player& p, Monster& m, char* name, \
int r1, int c1, int r, int c, int hp,int direction_picture_number)//传入名字
{
PK(p, m);
if (p.hp)//还有血量则继续执行
{
//改地图
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
PrintMap(Map[p.position[0]],p);
PrintInfor(name, hp - p.hp, m.money, m.exp); //图形化展示信息
p.money += m.money;
p.Exp += m.exp;
Judge_and_LvUp(p);
return 0;
}
//坐标重置回来
set_new_position(p.position, r1, c1);
return 2; //死亡
}
void MeetGodness(Player& p, int map[][COL], int r1,int c1,int r,int c) //遇见后要消失!
{
if (OfferMoneyTimes == 2 && meetPrincess==0)
{
//玩家坐标重置回来 //女神像不消失!
set_new_position(p.position, r1, c1);
return;
}
musicMeet();
int cost = 50* (OfferMoneyTimes + 1);
std::string message1 = "\n\nWould you like to offer \n" + std::to_string(cost) + " gold coins to the goddess?\n\nYes: 1 NO: 2";
std::string message2 = "\n\nWould you offer all your money\nto the goddess for a miracle?\n\nYes: 1 NO: 2";
char message3[] = "\n\n\nA new entrance is now opened";
if (OfferMoneyTimes < 2)
PrintSpecialInfor(message1.c_str());
//else if (OfferMoneyTimes == 2 && meetPrincess) //已经供奉了两次,必须在遇到公主之后
// PrintSpecialInfor(message2.c_str());
if (OfferMoneyTimes == 2 && meetPrincess)//第三次供奉,必须在遇到公主之后
{
PrintSpecialInfor(message2.c_str());
while (1)
{
Sleep(70);
if (GetAsyncKeyState(VK_NUMPAD1) & 0x8000) //同意
{
while (GetAsyncKeyState(VK_NUMPAD1) & 0x8000)
Sleep(30);
OfferMoneyTimes += 1;
map[r][c] = 7; //女神像消失
//玩家坐标重置回来
set_new_position(p.position, r1, c1);
p.money = 0;
musicLvUp();
Map[2][8][11] = 11;//第三楼出现楼梯,需要三维数组!
PrintSpecialInfor(message3);
exit();
return;
}
if (GetAsyncKeyState(VK_NUMPAD2) & 0x8000) //不同意
{
while (GetAsyncKeyState(VK_NUMPAD2) & 0x8000)
Sleep(30);
//玩家坐标重置回来
set_new_position(p.position, r1, c1);
return;
}
}
}
while (1) //前两次供奉
{
Sleep(70);
if (GetAsyncKeyState(VK_NUMPAD1) & 0x8000) //同意
{
while (GetAsyncKeyState(VK_NUMPAD1) & 0x8000)
Sleep(30);
if (p.money >= cost)
{
p.money -= cost;
OfferMoneyTimes += 1;
if (OfferMoneyTimes == 1)
{
map[r][c] = 7; //女神像消失
//玩家坐标重置回来
set_new_position(p.position, r1, c1);
PrintSpecialInfor("\n\n\nYou get nothing!");
Sleep(600);
return;
}//啥都没有,哈哈哈
else if (OfferMoneyTimes == 2)//得一个生命宝石
{
musicLvUp();
int defence = 2;
p.def += defence;
PrintLifeGem(defence);
//女神像不消失!
//玩家坐标重置回来
set_new_position(p.position, r1, c1);
return;
}
else
return;
}
else
{
//玩家坐标重置回来
set_new_position(p.position, r1, c1);
PrintSpecialInfor("\n\n\nYour money is not enough!");
Sleep(600);
return;
}
}
if (GetAsyncKeyState(VK_NUMPAD2) & 0x8000) //不同意
{
while (GetAsyncKeyState(VK_NUMPAD2) & 0x8000)
Sleep(30);
//玩家坐标重置回来
set_new_position(p.position, r1, c1);
return;
}
}
}
int ValidMove(int map[][COL], Player& p, int r, int c, int r1, int c1, \
int direction_picture_number,int direction) //direction 4 5 6 7分别表示上下左右
{
int hp = p.hp;
if (map[r][c] == 39) //找到公主——胜利
{
meetPrincess += 1;
set_new_position(p.position, r1, c1);//坐标重置回来
if (meetPrincess == 1)
{
musicMeet();
PrintFalsePrincess();
map[r][c] = 7;//假公主消失
return 0;
}
return 3;//胜利
}
//空地
if (map[r][c] == 7)
{
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
return 0;
}
if (map[r][c] == 51) //贤者
{
meetSage += 1;
set_new_position(p.position, r1, c1);//坐标重置回来
musicMeet();
if(meetSage==1)
{
char message1[] = "\n\nThe tower seems a bit strange\n\nI'm not sure if Princess is here";
char message2[] = "\n\nThe tower is extremely dangerous\n\nThis is the Magic Book";
char message3[] = "\n\nIt can turn souls of the monsters \n\nkilled into Exp to strengthen you";
char message4[] = "\n\nThe Monster Encyclopedia in it \n\ncontains the information I know\n(Press e to open or close)";
char message5[] = "\nAlso\n\nthere might be something good\n\n on the third floor";
char message6[] = "\n\nRemember,solving a problem is \n\nnever confined to a single way";
char message7[] = "\n\n\nGood luck to you!";
PrintSpecialInfor(message1);
exit();
PrintSpecialInfor(message2);
exit();
PrintSpecialInfor(message3);
exit();
PrintSpecialInfor(message4);
exit();
PrintSpecialInfor(message5);
exit();
PrintSpecialInfor(message6);
exit();
PrintSpecialInfor(message7);
exit();
}
else
{
//char message1[] = "\n\nGood job!\n\nYou've made it to the second floor";
char message1[] = "\n\nI've found Princess is here,\n\nbut I'm little confused...";
char message2[] = "\n\nBy the way,\n\nDef may weigh more than Ack";
char message3[] = "\n\nHere is a Moderate Healing Potion.\n\nGood luck!";
PrintSpecialInfor(message1);
exit();
PrintSpecialInfor(message2);
exit();
PrintSpecialInfor(message3);
exit();
//获得中等恢复药水
musicPickup();
if (p.hp + 50 > p.hp_limit)
{
p.hp = p.hp_limit;
}
else
{
p.hp += 50;
}
char name[] = "Moderate Healing Potion";
PrintInfor(name, p.hp - hp);
}
map[r][c] = 7;//贤者消失
return 0;
}
//钥匙
//黄色
if (map[r][c] == 4)
{
musicPickup();
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
p.yellow_key += 1;
return 0;
}
if (map[r][c] == 17) //蓝色钥匙
{
musicPickup();
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
p.blue_key += 1;
return 0;
}
//门:有没有钥匙
// 5——黄色门
if (map[r][c] == 5)
{
if (p.yellow_key < 1)
{
//坐标重置回来
set_new_position(p.position, r1, c1);
PrintNoKey();
return 0;
}
//有钥匙
musicEnter();
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
p.yellow_key -= 1;
return 0;
}
if (map[r][c] == 18) //蓝色门
{
if (p.blue_key < 1)
{
//坐标重置回来
set_new_position(p.position, r1, c1);
PrintNoKey();
return 0;
}
//有钥匙
musicEnter();
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
p.blue_key -= 1;
return 0;
}
//怪物:死亡或击败,击败会获得经验与金币 判断血量
//绿色史莱姆酱 6
if (map[r][c] == 6)
{
musicBattle();
char name[] = "Green Slime";//传入名字
return Battle(map, p, GreenSlime, name, r1, c1, r, c, hp, direction_picture_number);
}
//红色史莱姆酱 9
if (map[r][c] == 9)
{
musicBattle();
char name[] = "Red Slime";//传入名字
return Battle(map, p, RedSlime, name, r1, c1, r, c, hp, direction_picture_number);
}
//蝙蝠 30 bat B
if (map[r][c] == 30)
{
musicBattle();
char bt[] = "Bat";//传入名字
return Battle(map, p, Bat, bt, r1, c1, r, c, hp, direction_picture_number);
}
//骷髅 31 骷髅 S
if (map[r][c] == 31)
{
musicBattle();
char name[] = "Skeleton";//传入名字
return Battle(map, p, Skeleton, name, r1, c1, r, c, hp, direction_picture_number);
}
//骷髅士兵 32
if (map[r][c] == 32)
{
musicBattle();
char name[] = "Skeleton Soldier";//传入名字
return Battle(map, p, SkeletonSoldier, name, r1, c1, r, c, hp, direction_picture_number);
}
//巫师 33
if (map[r][c] == 33)
{
musicBattle();
char name[] = "Wizard";//传入名字
return Battle(map, p, Wizard, name, r1, c1, r, c, hp, direction_picture_number);
}
//魔王 38
if (map[r][c] == 38)
{
musicBattle();
char name[] = "Demon King";//传入名字
return Battle(map, p, DemonKing, name, r1, c1, r, c, hp, direction_picture_number);
}
//不可战胜的魔王 54
if (map[r][c] == 54)
{
musicBattle();
char name[] = "Demon Emperor";//传入名字
return Battle(map, p, DemonEmperor, name, r1, c1, r, c, hp, direction_picture_number);
}
// 药水 2 - minor_healing_potion h 3 - moderate_healing_potion H
//minor
if (map[r][c] == 2)
{
musicPickup();
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
if (p.hp + 20 > p.hp_limit)
{
p.hp = p.hp_limit;
}
else
{
p.hp += 20;
}
PrintMap(Map[p.position[0]], p);
char name[] = "Minor Healing Potion";
PrintInfor(name, p.hp - hp);
_kbhit();
return 0;
}
//moderate
if (map[r][c] == 3)
{
musicPickup();
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
if (p.hp + 50 > p.hp_limit)
{
p.hp = p.hp_limit;
}
else
{
p.hp += 50;
}
PrintMap(Map[p.position[0]], p);
char name[] = "Moderate Healing Potion";
PrintInfor(name, p.hp - hp);
return 0;
}
if (map[r][c] == 22)//盾
{
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
p.def += 10;
musicPickup();
PrintMap(Map[p.position[0]], p);
char name[] = "Boko Shield";
PrintShield(name,10); //防御加10
return 0;
}
if (map[r][c] == 21)//剑
{
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
p.atk += 10;
musicPickup();
PrintMap(Map[p.position[0]], p);
char name[] = "Boko Sword";
PrintSword(name, 10); //攻击加10
return 0;
}
if (map[r][c] == 24)//镐子
{
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
p.pickaxe += 1;
musicPickup();
PrintMap(Map[p.position[0]], p);
char message[] = "\n\n\nYou get a Pickaxe!";
PrintSpecialInfor(message);
exit();
return 0;
}
if (map[r][c] == 20)//防御宝石
{
int defence = 2;
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
p.def += defence;
musicPickup();
PrintMap(Map[p.position[0]], p);
PrintLifeGem(defence);
return 0;
}
if (map[r][c] == 19)//攻击宝石
{
int attack = 2;
map[r1][c1] = 7;
map[r][c] = direction_picture_number;
p.atk += attack;
musicPickup();
PrintMap(Map[p.position[0]], p);
PrintAttackGem(attack);
return 0;
}
if (map[r][c] == 50) //女神像
{
MeetGodness(p,map,r1,c1,r,c);
return 0;
}
//上楼或下楼 11 upstairs + 12 downstairs -
//上
if (map[r][c] == 11)
{
map[r1][c1] = 7;
p.position[0] += 1; //加一层
return direction;
}
//下
if (map[r][c] == 12)
{
map[r1][c1] = 7;
p.position[0] -= 1; //加一层
return direction;
}
return 0;
}
int afterPress(int map[][COL], Player& p,int r,int c,int r1,int c1,\
int direction_picture_number,int direction)//4 5 6 7分别表示上下左右
{
if (judge_hit(map[r][c]))//没撞墙则执行
{
set_new_position(p.position, r, c);
return ValidMove(map, p, r, c, r1, c1, direction_picture_number,direction);
}
//重置
r = r1;c = c1;
return 19;//表示无效输入
}
int play(int map[][COL],Player &p) //玩家移动
{
int r = p.position[1];
int c = p.position[2];
//direction
int up = 4;int down = 5;int left = 6;int right = 7;
//备份
int r1 = r;
int c1 = c;
int hp = p.hp;
int ret; //接收afterPress的值,并判断
//按一个键走好几步——通过sleep解决
while (true)
{
Sleep(70);
if ((GetAsyncKeyState(VK_LEFT) & 0x8000) ||(GetAsyncKeyState('A') & 0x8000))
{ // 检测左键,a不分大小写,都可以实现。注意(GetAsyncKeyState('a')有问题,不起作用!
while ((GetAsyncKeyState(VK_LEFT) & 0x8000) || (GetAsyncKeyState('A') & 0x8000))
{
Sleep(30);
}//实现一键一动,松开时才移动
ret=afterPress(map, p, r, c - 1, r1, c1,35,left);//35是向左的素材的编号
if (ret != 19)
return ret;
}
else if ((GetAsyncKeyState(VK_RIGHT) & 0x8000) || (GetAsyncKeyState('D') & 0x8000))
{ // 检测右键
while ((GetAsyncKeyState(VK_RIGHT) & 0x8000) || (GetAsyncKeyState('D') & 0x8000))
{
Sleep(30);
}
ret = afterPress(map, p, r, c + 1, r1, c1,36,right);//向右
if (ret != 19)
return ret;
}
else if ((GetAsyncKeyState(VK_UP) & 0x8000) || (GetAsyncKeyState('W') & 0x8000))
{ // 检测上键
while ((GetAsyncKeyState(VK_UP) & 0x8000) || (GetAsyncKeyState('W') & 0x8000))
{
Sleep(30);
}
ret = afterPress(map, p, r-1, c , r1, c1,37,up);
if (ret != 19)
return ret;
}
else if ((GetAsyncKeyState(VK_DOWN) & 0x8000) || (GetAsyncKeyState('S') & 0x8000))
{ // 检测下键
while ((GetAsyncKeyState(VK_DOWN) & 0x8000) || (GetAsyncKeyState('S') & 0x8000))
{
Sleep(30);
}
ret = afterPress(map, p, r + 1, c, r1, c1,8,down);
if (ret != 19)
return ret;
}
//镐子 p
else if (GetAsyncKeyState('P') & 0x8000)
{ // 检测p键
while (GetAsyncKeyState('P') & 0x8000)
{
Sleep(30);
}
if (p.pickaxe <= 0)
continue;
char message[] = "\n\n\nYou have destroyed a wall!";
PrintUsePickaxe();
//Sleep(200);
while (true)
{
if (GetAsyncKeyState('P') & 0x8000)
{//再次按 p 取消
while (GetAsyncKeyState('P') & 0x8000)
{
Sleep(30);
}
return 0;
}
else if ((GetAsyncKeyState(VK_LEFT) & 0x8000)|| (GetAsyncKeyState('A') & 0x8000))
{ // 镐子 左键
while ((GetAsyncKeyState(VK_LEFT) & 0x8000)|| (GetAsyncKeyState('A') & 0x8000))
{
Sleep(30);
}
if (map[r][c - 1] == 1)
{
PrintMessage(message);
map[r][c - 1] = 7;
p.pickaxe -= 1;
}
return 0;
}
else if ((GetAsyncKeyState(VK_RIGHT) & 0x8000) ||(GetAsyncKeyState('D') & 0x8000))
{ // 镐子 右键
while ((GetAsyncKeyState(VK_RIGHT) & 0x8000) || (GetAsyncKeyState('D') & 0x8000))
{
Sleep(30);
}
if (map[r][c + 1] == 1)
{
PrintMessage(message);
map[r][c + 1] = 7;
p.pickaxe -= 1;
}
return 0;
}
else if ((GetAsyncKeyState(VK_UP) & 0x8000)|| (GetAsyncKeyState('W') & 0x8000))
{ // 镐子 上键
while ((GetAsyncKeyState(VK_UP) & 0x8000)|| (GetAsyncKeyState('W') & 0x8000))
{
Sleep(30);
}
if (map[r - 1][c] == 1)
{
PrintMessage(message);
map[r - 1][c] = 7;
p.pickaxe -= 1;
}
return 0;
}
else if ((GetAsyncKeyState(VK_DOWN) & 0x8000) || (GetAsyncKeyState('S') & 0x8000))
{ // 镐子 下键
while ((GetAsyncKeyState(VK_DOWN) & 0x8000) || (GetAsyncKeyState('S') & 0x8000))
{
Sleep(30);
}
if (map[r + 1][c] == 1)
{
PrintMessage(message);
map[r + 1][c] = 7;
p.pickaxe -= 1;
}
return 0;
}
}
}
//图鉴 e
else if ((GetAsyncKeyState('E') & 0x8000) && meetSage)
{ // 检测e键
while (GetAsyncKeyState('E') & 0x8000)
{
Sleep(30);
}
musicEncyclopedia();
MonsterEncyclopedia(p);
return 0;
}
//退出!
else if (GetAsyncKeyState(VK_ESCAPE) & 0x8000)
{
return -12;
}
}
return 0;
}
//此处传进来的map是所有楼层的!
void ChangeFloor(int Map[FLOOR][ROW][COL],Player& P,int ret)
{
musicEnter();
if (ret == 4 && Map[P.position[0]][P.position[1] - 1][P.position[2]] == 7)//向上并且楼梯上一格是空
{
P.position[1] -= 1;
Map[P.position[0]][P.position[1]][P.position[2]] = 8;
}
else if (ret == 5 && Map[P.position[0]][P.position[1] + 1][P.position[2]] == 7)
{
P.position[1] += 1;
Map[P.position[0]][P.position[1]][P.position[2]] = 8;
}
else if (ret == 6 && Map[P.position[0]][P.position[1]][P.position[2] - 1] == 7)
{
P.position[2] -= 1;
Map[P.position[0]][P.position[1]][P.position[2]] = 8;
}
else if (ret == 7 && Map[P.position[0]][P.position[1]][P.position[2] + 1] == 7)
{
P.position[2] += 1;
Map[P.position[0]][P.position[1]][P.position[2]] = 8;
}
//默认位置:门上一格是空,门上,否则去其他的空地
else if (Map[P.position[0]][P.position[1] - 1][P.position[2]] == 7)
{
P.position[1] -= 1;
Map[P.position[0]][P.position[1]][P.position[2]] = 8;
}
else if (Map[P.position[0]][P.position[1] + 1][P.position[2]] == 7)
{
P.position[1] += 1;
Map[P.position[0]][P.position[1]][P.position[2]] = 8;
}
else if (Map[P.position[0]][P.position[1]][P.position[2] + 1] == 7)
{
P.position[2] += 1;
Map[P.position[0]][P.position[1]][P.position[2]] = 8;
}
else
{
P.position[2] -= 1;
Map[P.position[0]][P.position[1]][P.position[2]] = 8;
}
}
1.使用BeginBatchDraw() 与 EndBatchDraw()减少绘图过程中的屏幕刷新次数,从而提高性能。
2.RGB用来控制颜色,代码中使用它来实现颜色渐变
#include"head.hpp"
IMAGE images[60];
void LoadImagesHelper(int n,int Width,int Height)
{
//std::string location = "D:\\Merrik\\code\\小游戏\\Tower of the Sorcerer\\materials\\" + std::to_string(n) + ".png";
std::string location = std::to_string(n) + ".png";
loadimage(&images[n], location.c_str(), Width, Height); //最后两个数字表示缩放后的大小
}
void LoadImages() // 加载图片素材
{
for (int i = 0;i <= 40;i++)
LoadImagesHelper(i,50,50);
//大图标,用于显示
//loadimage(&images[49], "D:\\Merrik\\code\\小游戏\\Tower of the Sorcerer\\materials\\8.png", 100, 100);
for (int i = 41;i <= 48;i++)
LoadImagesHelper(i, 100, 100);
for (int i = 50;i <= 54;i++)
LoadImagesHelper(i, 50, 50);
//开始界面
loadimage(&images[49], "49.jpg", 850, 650);
}
RECT R = { 0, 00, 850, 650 };//矩形指针,用于打印游戏开始画面
void StartImage()
{
BeginBatchDraw();
//setbkcolor(BLACK);
setfillcolor(BLACK);
setbkmode(TRANSPARENT);
//solidrectangle(0, 00, 850, 650); // 填充矩形
putimage(0, 0, &images[49]);//
settextstyle(70, 0, "Consolas");
drawtext("\nTower of Sorcerer", &R, DT_CENTER);
EndBatchDraw();
BeginBatchDraw();
while (1)
{
Sleep(70);
if ((GetAsyncKeyState(VK_SPACE) & 0x8000) || (GetAsyncKeyState(VK_RETURN) & 0x8000))
break;
}
settextstyle(30, 0, "Consolas");
drawtext(\
"\n\n\n\n\n\n\n\n\n\n\n\n\n\nThe princess has been captured and imprisoned by the Demon.\nBut nobody knows where she is.\nYou've found clues leading to this tower\n so you come here ..."\
, &R, DT_CENTER); //The princess has been captured by the devil You found clues about her that led to this tower , Please use your wisdom and courage //删除了\n\n(Press space key to continue)
EndBatchDraw();
Sleep(500);
while (1)
{
Sleep(70);
if ((GetAsyncKeyState(VK_SPACE) & 0x8000) || (GetAsyncKeyState(VK_RETURN) & 0x8000))
break;
}
}
void InventInfor() //打印制作信息
{
BeginBatchDraw();
cleardevice();
settextstyle(70, 0, "Consolas");
drawtext("\n\n Designed by Merrik.", &R, DT_CENTER);
settextstyle(40, 0, "Consolas");
drawtext("\n\n\n\n\n\n\n\n\n\nThis game would not have been possible\n without the support of my dear roomates\nor the advice of Leanne", &R, DT_CENTER);//
EndBatchDraw();
//musicGameLoading();
Sleep(5000);
//mciSendString("close GameLoading ", NULL, 0, NULL);
}
void InitImage()
{
BeginBatchDraw();
cleardevice();
//边框上的图片
for (int i = 0; i < ROW + 4; ++i) //上边框
putimage(i * 50, 0, &images[0]);
for (int i = 0; i < ROW + 4; ++i) //下边框
putimage(i * 50, 600, &images[0]);
for (int i = 1; i <= COL -1; ++i) //左边框
putimage(0, i * 50, &images[0]);
for (int i = 1; i <= COL - 1; ++i) //右边框
putimage(800, i * 50, &images[0]);
//for (int i = 1; i <= COL - 1; ++i) //地图左框
// putimage(200, i * 50, &images[0]);
putimage(50, 50, &images[48]); //人
putimage(50, 150, &images[14]); //hp
putimage(50, 200, &images[40]); //hp_limit
putimage(50, 250, &images[13]); //atk
putimage(50, 300, &images[15]); //def
putimage(50, 350, &images[16]); //money
putimage(50, 400, &images[4]); // yellow key
putimage(50, 450, &images[17]);//blue key
putimage(50, 500, &images[25]);// red key
putimage(50, 550, &images[24]); //镐子
EndBatchDraw();
}
//辅助函数,用于右对齐
int countDigits(int number) {
if (number == 0) return 1; // 0是一个特殊情况,它有1位
int count = 0;
while (number > 0) {
number /= 10;
++count;
}
return count;
}
void PrintMap(int map[][COL], Player& p)
{
BeginBatchDraw();
for (int i = 1; i < ROW-1; ++i) { //边框都是固定的,不用重复打印
for (int j = 1; j < COL-1; ++j)
{
putimage(200+j * 50, i * 50, &images[map[i][j]]); // 显示图像
}
}
for (int i = 1; i <= COL - 1; ++i) //地图左框
putimage(200, i * 50, &images[0]);
settextcolor(WHITE);
settextstyle(40, 0, "Consolas");
outtextxy(155, 50, "Lv");
settextstyle(30, 0, "Consolas");
setfillcolor(getbkcolor());
solidrectangle(150, 100, 200, 150);
solidrectangle(100, 150, 165, 600); //先清空之前打印的数据
outtextxy(190 - 15 * countDigits(p.Lv), 100, std::to_string(p.Lv).c_str()); //把数字转化成字符串
outtextxy(165 - 15 * countDigits(p.hp), 160, std::to_string(p.hp).c_str());
outtextxy(165 - 15 * countDigits(p.hp_limit), 210, std::to_string(p.hp_limit).c_str());
outtextxy(165 - 15 * countDigits(p.atk), 260, std::to_string(p.atk).c_str());
outtextxy(165 - 15 * countDigits(p.def), 310, std::to_string(p.def).c_str());
outtextxy(165 - 15 * countDigits(p.money), 360, std::to_string(p.money).c_str());
outtextxy(165 - 15 * countDigits(p.yellow_key), 410, std::to_string(p.yellow_key).c_str());
outtextxy(165 - 15 * countDigits(p.blue_key), 460, std::to_string(p.blue_key).c_str());
outtextxy(165 - 15 * countDigits(p.red_key), 510, std::to_string(p.red_key).c_str());
outtextxy(165 - 15 * countDigits(p.pickaxe), 560, std::to_string(p.pickaxe).c_str());
EndBatchDraw();
}
RECT R1 = { 220,230,720, 450 };//矩形指针R1,用于打印游戏进程中信息
void PrintMessage(const char* message)
{
setbkmode(TRANSPARENT); //设置字体为透明
settextcolor(BLACK);
for (int i = 256;i > 64;i -= 5)
{ //双缓冲技术来避免绘制时图像闪烁,效果显著
BeginBatchDraw(); //与下面的EndBatchDraw();必须成对出现
settextstyle(30, 0, "Consolas");
setfillcolor(RGB(i, 0, 0));//第一个数字表示红色
solidrectangle(220, 230, 720, 450); // 填充矩形
drawtext(message, &R1, DT_CENTER);
//在矩形区域R1内输入文字,水平居中,垂直居中
Sleep(30);
EndBatchDraw();
}
}
void PrintInfor(char*name,int d_hp,int money,int exp) //Battle
{
std::string n = name; // 将字符指针转换为std::string
std::string message = "\n\nYou've beaten the " + n + "\nYou lost " + std::to_string(d_hp) + \
" HP in the battle\nYou get " + std::to_string(money) + " gold coins\n";
PrintMessage(message.c_str());
}
void PrintInfor(char* name, int addHP) //potion
{
//"You find a minor healing potion and get 20 hp"
std::string n = name; // 将字符指针转换为std::string
std::string message = "\n\nYou find a\n" + n + "\nand get " + std::to_string(addHP) + " HP";
PrintMessage(message.c_str());
}
void PrintNoKey()//no enough key
{
std::string message = "\n\nYou don't have enough keys! \nGo around and find one!";
PrintMessage(message.c_str());
}
void PrintLvUp()
{
setbkmode(TRANSPARENT); //设置字体为透明
settextcolor(RED);
for (int i = 256;i > 64;i -= 5)
{ //双缓冲技术来避免绘制时图像闪烁,效果显著
BeginBatchDraw(); //与下面的EndBatchDraw();必须成对出现
setfillcolor(RGB(i, i, 0));
solidrectangle(220, 230, 720, 450); // 填充矩形
settextstyle(50, 0, "Consolas");
drawtext("\nLv up!", &R1, DT_CENTER);
settextstyle(30, 0, "Consolas");
drawtext("\n\n\n\nYou get 2 Atk 2 Def!\nMax HP limit increased by 100", &R1, DT_CENTER);
Sleep(30);
EndBatchDraw();
}
}
void PrintLifeGem(int defence)
{
std::string message = "\n\nYou get a Life Gem \n\nand get " + std::to_string(defence)+" Def";
PrintMessage(message.c_str());
}
void PrintAttackGem(int attack)
{
std::string message = "\n\nYou get an Attack Gem \n\nand get "+ std::to_string(attack) + " Atk";
PrintMessage(message.c_str());
}
void PrintSpecialInfor(const char* name)
{
BeginBatchDraw();
setbkmode(TRANSPARENT);
settextcolor(BLACK);
setfillcolor(RGB(255, 255, 0));
solidrectangle(220, 230, 720, 450); // 填充矩形
settextstyle(30, 0, "Consolas");
drawtext(name, &R1, DT_CENTER);
EndBatchDraw();
}
void exit()
{
Sleep(500);
while (1)
{
Sleep(70);
if (GetAsyncKeyState(VK_SPACE) & 0x8000)
break;
}
}
void PrintFalsePrincess()
{
char message[] = "\n\n You reached here in a good mood\n\nthinking you've made it...";
char message2[] = "\n\n\nbut...";
char message3[] = "\n\nThe princess is fake.\n\n The true one is hided elsewhere!";
char message4[] = "\n\nYou have to utilize your wisdom\n\nto find the true one";
PrintSpecialInfor(message);
exit();
PrintSpecialInfor(message2);
exit();
PrintSpecialInfor(message3);
exit();
PrintSpecialInfor(message4);
exit();
}
void PrintLose()
{
char message[] = "\n\nIt's a reget that\n\n you have lost your life.";
PrintSpecialInfor(message);
exit();
}
void PrintWin()
{
char message[] = "\nCongratulations!\n\n With your great effort\n you've saved the true princess!";
PrintSpecialInfor(message);
exit();
}
void PrintShield(char* name, int defence)
{
std::string n = name;
std::string message = "\n\nYou get a " + n + "\n\nand get " + std::to_string(defence) + " Def";
PrintSpecialInfor(message.c_str());
exit();
}
void PrintSword(char* name, int attack)
{
std::string n = name;
std::string message = "\n\nYou get a " + n + "\n\nand get " + std::to_string(attack) + " Atk";
PrintSpecialInfor(message.c_str());
exit();
}
void PrintUsePickaxe()
{
setbkmode(TRANSPARENT);
settextcolor(WHITE);
settextstyle(40, 0, "Consolas");
drawtext("\n\nUsing Pickaxe", &R1, DT_CENTER);
}
//图鉴
RECT R2 = { 300, 100, 750, 550 };
void MonsterEncyclopedia(Player& p)
{
BeginBatchDraw();
setbkmode(TRANSPARENT);
settextcolor(BLACK);
setfillcolor(RGB(255, 255, 0));
solidrectangle(300, 80, 750, 580); // 填充矩形
settextstyle(35, 0, "Consolas");
drawtext("Monster Encyclopedia", &R2, DT_CENTER);
putimage(320, 150, &images[41]); //绿色史莱姆
putimage(320, 250, &images[42]); //红色史莱姆
putimage(320, 350, &images[43]); //蝙蝠
putimage(320, 450, &images[47]); //骷髅
putimage(540, 150, &images[46]); //巫师
putimage(540, 250, &images[44]); //骷髅士兵
putimage(540, 350, &images[45]); //魔王
settextstyle(30, 0, "Consolas");
for(int i=0;i<4;i++)
outtextxy(430, 160+100*i, "Damage");
for (int i = 0;i < 3;i++)
outtextxy(650, 160 + 100 * i, "Damage");
int d = Damage(p, GreenSlime); //打印数值
outtextxy(500 - 15 * countDigits(d), 210, std::to_string(d).c_str());
d = Damage(p, RedSlime);
outtextxy(500 - 15 * countDigits(d), 310, std::to_string(d).c_str());
d = Damage(p, Bat);
outtextxy(500 - 15 * countDigits(d), 410, std::to_string(d).c_str());
d = Damage(p, Skeleton);
outtextxy(500 - 15 * countDigits(d), 510, std::to_string(d).c_str());
d = Damage(p, Wizard);
outtextxy(720 - 15 * countDigits(d), 210, std::to_string(d).c_str());
d = Damage(p, SkeletonSoldier);
outtextxy(720 - 15 * countDigits(d), 310, std::to_string(d).c_str());
//d = Damage(p, DemonKing);
outtextxy(720 - 45, 410,"???");
EndBatchDraw();
while (1)
{
Sleep(70);
if ((GetAsyncKeyState(VK_SPACE) & 0x8000)|| (GetAsyncKeyState('e') & 0x8000)\
|| (GetAsyncKeyState('E') & 0x8000))
{
while ((GetAsyncKeyState(VK_SPACE) & 0x8000) || (GetAsyncKeyState('e') & 0x8000)\
|| (GetAsyncKeyState('E') & 0x8000))
{
Sleep(30);
}
break;
}
}
}
#include"head.hpp"
void musicGameLoading() //真奇怪了,播放有卡顿?难道长一点的音频都不行吗
{
mciSendString("open GameLoading.mp3 alias GameLoading", NULL, 0, NULL);
mciSendString("play GameLoading wait", NULL, 0, NULL);
mciSendString("close GameLoading ", NULL, 0, NULL);
}
void musicPickup()
{
mciSendString("close pickup", NULL, 0, NULL);
mciSendString("open pickup.mp3 alias pickup", NULL, 0, NULL);
mciSendString("play pickup ", NULL, 0, NULL);
}
void musicBattle()
{
mciSendString("open battle.mp3 alias battle", NULL, 0, NULL);
mciSendString("play battle wait", NULL, 0, NULL);
mciSendString("close battle", NULL, 0, NULL);
}
void musicEnter()
{
mciSendString("close enter", NULL, 0, NULL);
mciSendString("open in.mp3 alias enter", NULL, 0, NULL);
mciSendString("play enter", NULL, 0, NULL);
}
void musicLvUp()
{
mciSendString("close LvUp", NULL, 0, NULL);
mciSendString("open LvUp.mp3 alias LvUp", NULL, 0, NULL);
mciSendString("play LvUp", NULL, 0, NULL);
}
void musicDie()
{
mciSendString("close die", NULL, 0, NULL);
mciSendString("open die.mp3 alias die", NULL, 0, NULL);
mciSendString("play die", NULL, 0, NULL);
}
void musicWin()
{
mciSendString("close win", NULL, 0, NULL);
mciSendString("open win.mp3 alias win", NULL, 0, NULL);
mciSendString("play win", NULL, 0, NULL);
}
void musicEncyclopedia()
{
mciSendString("close Encyclopedia", NULL, 0, NULL);
mciSendString("open Encyclopedia.mp3 alias Encyclopedia", NULL, 0, NULL);
mciSendString("play Encyclopedia", NULL, 0, NULL);
}
void musicMeet()
{
mciSendString("close Godness", NULL, 0, NULL);
mciSendString("open Godness.mp3 alias Godness", NULL, 0, NULL);
mciSendString("play Godness", NULL, 0, NULL);
}
void closeMusic()
{
mciSendString("close GameLoading ", NULL, 0, NULL);
mciSendString("close pickup", NULL, 0, NULL);
mciSendString("close battle", NULL, 0, NULL);
mciSendString("close enter", NULL, 0, NULL);
mciSendString("close LvUp", NULL, 0, NULL);
mciSendString("close die", NULL, 0, NULL);
mciSendString("close win", NULL, 0, NULL);
mciSendString("close Encyclopedia", NULL, 0, NULL);
mciSendString("close Godness", NULL, 0, NULL);
mciSendString("close background", NULL, 0, NULL);
}
储存玩家、怪物属性,以及地图。玩家和怪物属性改动一定要谨慎,否则游戏难度会失衡
#include"head.hpp"
//可以考虑把地图放进文档中,通过读文档来进行地图生成
Monster::Monster(int hp, int atk, int def, int exp, int gold/* char* name*/) : hp(hp), atk(atk), def(def), exp(exp), money(gold) {
// 这里可以添加一些初始化代码,如果需要的话
}
//创建对象
Monster GreenSlime(10, 7, 1, 5, 1);
Monster RedSlime(10, 10, 2, 10, 2);
Monster Bat(15, 15, 2, 15, 3);
Monster Skeleton(50, 20, 3, 20, 5);
Monster Wizard(50, 25, 3, 25, 7);
Monster SkeletonSoldier(60, 50, 5, 35, 10);
Monster DemonKing(400, 50, 5, 100, 30);
Monster DemonEmperor(1000, 100, 20, 500, 100);
//Monster Sele();
Player::Player()
{ //初始:第一层 第六行 第二列 position[0] = 0; position[1] = 11; position[2] = 7;
//测试位置,公主房间 position[0] = 2; position[1] = 8; position[2] = 10;
position[0] = 0; position[1] = 11; position[2] = 7;
hp = 100; hp_limit = hp; atk = 5; def = 0; money = 0;
Lv = 1; Exp = 0; yellow_key = 0; blue_key = 0;
red_key = 0; pickaxe = 0;
}
//地图,13*13,周围一圈为边界,中间的11*11为主地图r
//0 - border # 1 - wall # 2 - minor_healing_potion h 3 - modera_healing_potion H
// 4 - yellow key 'k' 5 - yellow door 'd'
//6 - green slime 's ' 7 - area ' ' 8 - player P
//10 magma M 11 upstairs + 12 downstairs -
//30及以后全是怪物 30 bat B 31 骷髅 S
int Map[FLOOR][ROW][COL] = {
{//1
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,7,31,5,7,7,7,32,5,7,30,4,0},
{0,2,7,1,22,7,1,1,1,11,1,5,0},
{0,1,5,1,19,3,1,3,1,30,1,19,0},
{0,20,7,1,4,4,1,5,1,30,1,5,0},
{0,7,30,1,1,1,1,7,5,7,2,7,0},
{0,1,5,1,4,7,6,7,1,1,1,1,0},
{0,9,7,1,7,1,1,1,1,4,4,20,0},
{0,7,1,1,6,1,3,30,9,7,7,9,0},
{0,51,7,7,7,1,1,7,1,1,7,7,0},
{0,4,1,7,1,1,4,7,7,7,6,7,0},
{0,3,1,6,9,7,7,8,4,1,2,3,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0}
},
{//2
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0, 7,30,6, 7,7,7,9,7, 7,7,4,0},
{0, 7, 1,1,32,1,5,1,1,12,7,51,0},
{0, 7,19,1,19,1,6,7,1, 1,1,1,0},
{0,31, 7,1,20,1,1,5,1,20,7,50,0},
{0, 7,9,1, 7,3,1,7,5, 7,11, 7,0},
{0, 1,5,1,1,1,1,30,1,1,1,5,0},
{0, 7,7,1,3,7,1,17,1,2,7,6,0},
{0, 7,1,1,1,5,1,1,1,7,9,7,0},
{0, 7,1,30,30,19,1,20,33,30,7,7,0},
{0, 7,7,7,1,7,1,5,1,7,7,31,0},
{0,4,3,7,5,7,6,7,7,7,9,2,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0}
},
{//3
{ 0,0,0,0,0,0,0,0,0,0,0,0,0 },
{0,3,33,7,7,5,9,7,30,2,1,3,0},
{0,33,7,7,7,1,7,9,7,7,1,21,0},
{0,7,7,7,33,1,5,1,1,31,1,18,0},
{0,31,33,31,17,1,30,19,1,7,7,7,0},
{0,1,5,1,1,1,19,3,1,7,12,7,0},
{0,2,7,20,19,1,53,53,53,53,53,53,0},
{0,7,7,7,4,4,53,7,7,39,7,7,0},
{0,33,7,24,7,50,53,7,7,7,7,7,0},//改回来!
{0,18,1,1,7,7,53,53,53,38,53,53,0},
{0,3,3,1,31,31,1,32,32,7,32,33,0},
{0,3,22,1,31,32,18,7,7,7,32,33,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0}
},
{//4
{0,0,0,0,0,0,0,0,0,0,0,0,0},
{0, 38,54,18, 7,7,7,7,7, 1,32,7,0},
{0, 7, 38,1,7,7,7,7,7,52,32,7,0},
{0, 7, 32,1,7,7,39,7,7,1,32,7,0},
{0, 7, 32,52,7,7,7,7,7,52,32,7,0},
{0, 7,32,1, 52,1,52,1,52, 1,32, 7,0},
{0, 7,32,32,32,32,32,32,32,32,32,7,0},
{0, 7,7,7,7,7,7,7,7,7,7,7,0},
{0, 32,7,7,7,7,7,7,7,7,7,12,0},
{0, 38,32,7,7,7,7,7,7,7,7,7,0},
{0, 38,38,32,7,7,7,7,7,3,7,7,0},
{0, 17,38,38,32,7,7,7,7,7,7,7,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0}
}
};
如果还想让游戏更有趣味性,可以参考以下意见:创建不同属性的角色? 增加宝箱,随机生成效果? 增加解谜?存档? 副手武器? 武器效果? 炸弹? 技能?职业? 魔药? 多门? 隐藏地图? 多地图?
最后,祝大家顺利完成属于自己的独特魔塔!