写在开头

重新以时间线的形式整理一下去年使用c++的SFML库制作月圆之夜(游戏程序设计大作业)的开发过程,括号里面是新的补充以及对一年前自己的吐槽

因为是在大二转专业后做首次接触游戏开发后才做的,当时c++学习得并不好,所以代码很乱很糟糕,许多思路也不是很清晰,完全是摸爬滚打混过来的,最后也有很多bug,不过还是一次很有收获的经历

当时也尝试着学习用游戏引擎做游戏,还觉得游戏引擎太难用了,现在想想游戏引擎是真的方便,真香

2020年4月6日

昨天做完扫雷后,思考了一下游戏程序设计的课程设计应该做什么。虽然老师的要求是做棋牌游戏,可是我感觉做卡牌游戏也不是不可以,说不定斗地主的玩法配上欧美魔幻画风也能成为一款卡牌大作呢。想到做自己常玩的卡牌游戏杀戮尖塔,月圆之夜,昆特牌,好像也就那么几个,想来想去觉得月圆之夜的游戏素材更好提取一些,直接手机上截图然后抠图就行了,因为我没有pc上的存档(我当初为什么不做斗地主!虽然但是我感觉斗地主也不好做…为什么要给自己挖坑!为什么不找个人组队!)

可以看到在图鉴出点开每张卡片,它们的位置都是固定的,所以我们就点开每张图片,然后截图,发到电脑上,然后再截取卡牌的区域,没错就是这么简单粗暴(我记得当时找了好久的图包没找到,没办法只好自己动手了,其实一开始是打算做昆特牌的,但是还是因为图片素材的原因,找不到图包,而且我当时巫师三里昆特牌没全收集,图也不好截就放弃了…)

于是下面这个脚本便诞生了(很有仪式感)

import cv2
import os
import numpy as npfileList=os.listdir("imgs2")
i=0
for file in fileList:im1 = cv2.imread("imgs2/"+file)im2 = im1[420:1185, 286:786]x, y = im2.shape[0:2]im3 = cv2.resize(im2, (int(y / 2.5), int(x / 2.5 / 306 * 300)))cv2.imwrite("output2/"+"card"+str(i)+".png",im3)i+=1

简述之就是遍历文件夹里的文件,然后截取范围,然后再缩放,因为素材的大小要适合而且宽高最好是整数,最后再输出

看看原始目录下的文件

输出目录的文件

(还用到了批量重命名工具)

这样我们就可以方便的使用了

4月7日

图片素材

游戏背景

直接用的游戏界面截图,截图一定要把握好时机(其实可以录屏再截屏的…)

准备了3张图片,分别对应游戏开始界面,随机地牢界面,不过还没考虑好要不要做这一部分,毕竟工程量很大(果不其然砍掉了,画大饼有一手的),战斗界面的背景

主界面

背景图上面已经准备了

因为我们是阉割版,所以只需要一个开始游戏的按钮

然后准备两个不同颜色文字的贴图

战斗界面

先看一下战斗界面的样子(跟最后的效果比起来简直是…)

角色属性

我们要将其中底部的属性UI部分抠出来,然后做一些优化

如果只是抠图的话我常用的叫做稿定设计,网页版一键抠图不用花半天时间打开ps(现在要付费才能用了!!)

抠好之后差不多就是这个样子

本来左边两个按钮是角色使用技能,感觉做这一部分应该挺麻烦的就直接省掉好了

回合结束按钮

当然,还得把结束回合的按钮抠出来

抠出来后我们准备三个不同状态的按钮,分别代表正常,悬浮,按下,可以修改图片的饱和度,曝光等来实现不同状态的感觉

差不多就是下面这个样子

敌人属性

做到这里突然想到hp,mp的变化应该怎么做,思考了一下打算把空槽的贴图横向拉伸然后来覆盖到hp,mp的位置(因为想保留血条上的气泡…你可真是个小机灵鬼)

先准备好空槽的贴图

再修改一下敌人的属性,改成矩形,方便我们进行贴图覆盖

然后是角色属性

敌人贴图

准备一个小木匠的图片~

总结

由于只是一个普通的课程设计作业,所以剔除了大部分的游戏内容来减少工程量,目前是打算只保留两套卡包,一个boss,一个角色,所以也就是普通的1v1的卡牌游戏的玩法,这周会抽出时间来搭建游戏的基本框架,先把界面都绘制起来,然后在细细雕琢,最后有时间的话再考虑要不要把删掉的玩法加进来(偷懒有一手的)

4月8日

素材补充

准备战斗时返回主界面的按钮和对话框以及其他的贴图

准备音乐素材

游戏背景音乐在网易云上有专辑,游戏音效就只能自己录了

功能实现

  1. 背景图绘制
  2. 背景音乐

搭建初始场景

做一个游戏游戏最重要最重要的,就是把游戏画面展现给玩家,所以我们所要做的第一步,就是绘制出游戏窗口,毕竟黑框框并不符合大多数人的审美~当然像《盲景》这种只用听的游戏就是例外了

所以我们还是像制作扫雷一样,定义主函数和一个类来进行游戏内容管理,像下面这样

首先是完善我们的Game.h

#pragma once
#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#include <windows.h>
#include <iostream>
#include <sstream>
using namespace sf;
typedef enum gameSceneState {       //不同的游戏场景SCENE_START, SCENE_FIGHT
};
class Game {public:sf::RenderWindow window;    //定义游戏窗口Game();~Game();int windowWidth, windowHeight;                        //场景宽,高int gameSceneState;                                   //场景状态bool gameOver, gameQuit;bool startMusic, fightBusic;                      //音乐播放状态Texture tGameStartBK, tGameFightBK, tStartBtn;      //加载纹理Sprite sGameStartBK, sGameFightBK, sStartBtn;     //精灵Music gameStartMusic;                               //音乐SoundBuffer victorSb, defeatSb, attackSb, hoverSb, pressSb, releaseSb;  //音效缓冲Sound victorSd, defeatSd, attackSd, hoverSd, pressSd, releaseSd;      //音效void Run();             //游戏运行void Initial();           //初始化void loadMediaData();  //加载媒体void Input();         //交互void Draw();            //绘制不同的内容void drawStart();      //初始场景void drawFight();     //战斗场景void Logic();};

然后是Game.cpp

#include "Game.h"
using namespace std;
Game::Game() {windowWidth = 1680;                              //窗口宽度windowHeight = 985;                              //窗口高度window.create(VideoMode(windowWidth, windowHeight), L"月圆之夜");           //建立一个xx宽xx高,标题为xxx的窗口,L表示双字节
}
Game::~Game() {}
void Game::Initial() {window.setFramerateLimit(60);                 //刷新帧数gameSceneState = SCENE_START;                    //初始默认游戏场景startMusic = true;fightBusic = false;gameOver = gameQuit = false;loadMediaData();if (startMusic) {gameStartMusic.play();gameStartMusic.setLoop(true);}
}
void Game::loadMediaData() {//加载贴图纹理if (!tGameStartBK.loadFromFile("data/bg/bg1.png")) {cout << "找不到data/bg/bg1.png" << endl;}//精灵绑定纹理sGameStartBK.setTexture(tGameStartBK);//加载音频if (!gameStartMusic.openFromFile("data/music/game.ogg")) {cout << "找不到data/music/game.ogg" << endl;}
}
void Game::Input() {Event event;while (window.pollEvent(event)) {               //接受事件if (event.type == Event::Closed) {window.close();                           //关闭键关闭窗口gameQuit = true;}if (event.type == sf::Event::EventType::KeyReleased && event.key.code == sf::Keyboard::Escape) {window.close();                          //按esc键关闭窗口gameQuit = true;}}
}
void Game::Draw() {if (gameSceneState == SCENE_START) {       //判断场景drawStart();} else if (gameSceneState == SCENE_FIGHT) {drawFight();}
}
void Game::drawStart() {        //初始场景window.clear();                           //清屏sGameStartBK.setPosition(0, 0);         //设置绘制位置window.draw(sGameStartBK);              //绘制window.display();                       //展示屏幕
}
void Game::drawFight() {        //战斗场景}
void Game::Logic() {}
void Game::Run() {do {Initial();while (window.isOpen()) {Input();Draw();Logic();}}while (!gameQuit);
}

最后主函数

#include<iostream>
#include"Game.h"int main() {Game game;while (game.window.isOpen()) {game.Run();}return 0;
}

这样就可以运行了

其他

主要是素材的问题

  1. sfml貌似只能加载ogg文件,所以还得都转换成ogg格式

    这时候我又找到一个好用的在线网站https://convertio.co/zh/

    它可以在线转换各种文件的格式

  2. win10操作ogg文件慢的要死(复制,删除要花好几分钟,不知道为啥),最后在win10商店安装“Web媒体扩展”应用成功解决问题

4月9日

功能实现

  1. 自定义按钮类
  2. 交互部分的优化
  3. 游戏场景的跳转
  4. 背景音乐管理

定义按钮类

既然我们做的是游戏,那么交互性一定是十分重要的,就拿界面上的按钮作比方,你不管按或者不按,它如果都是一个状态的话,那游戏体验的确会大打折扣(其实是要给玩家反馈,体现人机交互的视觉效果),所以我们最起码要给按钮做三种不同的状态(正常,悬浮,按下)。

而SFML中又没有button类,所以我们只能自己写了。

我们先定义头文件,并分别在Button.cpp和Game.h中引用,来写方法和实现实例化。

#pragma once
#include <SFML/Graphics.hpp>
using namespace sf;
class Button :public Sprite {   //继承SFML的Sprite类
public:Texture tNormal;         //三种不同状态的纹理Texture tHover;Texture tClick;void checkMouse(Vector2i, Event);                  //检查鼠标状态void setTextures(Texture, Texture);                 //加载纹理,两种状态void setTextures(Texture, Texture, Texture);      //加载纹理,三种状态
};

然后写方法(当时想检测不规则按钮可不可以检测点击点在图片上的alpha通道值来判断,因为不规则图案都是在一个矩形里的,但是也没去试)

#include "Button.h"
void Button::setTextures(Texture _tNormal, Texture _tClick) {tNormal = _tNormal;tClick = _tClick;setTexture(tNormal);     //默认加载普通纹理
}
void Button::setTextures(Texture _tNormal, Texture _tHover, Texture _tClick) {tNormal = _tNormal;tHover = _tHover;tClick = _tClick;setTexture(tNormal);      //默认加载普通纹理
}
void Button::checkMouse(Vector2i mouse, Event event) {//判断鼠标是不是在按钮内,前提是放正的矩形,一般情况下都是这样,如果是奇形怪状的需要再写别的方法if ((mouse.x > getPosition().x && mouse.x < getPosition().x + getTexture()->getSize().x) &&(mouse.y > getPosition().y && mouse.y < getPosition().y + getTexture()->getSize().y)) {if (event.type == Event::EventType::MouseButtonPressed && event.mouseButton.button == Mouse::Left) {setTexture(tClick);       //加载点击状态的纹理} else {setTexture(tHover);      //加载悬浮状态的纹理}} else {setTexture(tNormal);        //加载正常状态的纹理}
}

然后Game.h中定义我们三种纹理和按钮

Texture tStartBtnNormal, tStartBtnHover, tStartBtnClick;     //加载纹理
Button startBtn;                                                //自定义button类

最后在Game.cpp中添加

loadMediaData()

if (!tStartBtnNormal.loadFromFile("data/button/start.png")) {cout << "找不到data/button/start.png" << endl;
}
if (!tStartBtnHover.loadFromFile("data/button/startHover.png")) {cout << "找不到data/button/startHover.png" << endl;
}
if (!tStartBtnClick.loadFromFile("data/button/startClick.png")) {cout << "找不到data/button/startClick.png" << endl;
}
//精灵绑定纹理
startBtn.setTextures(tStartBtnNormal, tStartBtnHover, tStartBtnClick);

drawStart()

void Game::drawStart() {     //初始场景window.clear();                           //清屏startBtn.setPosition(1200, 200);window.draw(startBtn);window.display();                     //展示屏幕
}

Input()

void Game::Input() {Event event;Vector2i mousePosition = Mouse::getPosition(window);while (window.pollEvent(event)) {               //接受事件if (event.type == Event::Closed) {window.close();                           //关闭键关闭窗口gameQuit = true;}if (event.type == sf::Event::EventType::KeyReleased && event.key.code == sf::Keyboard::Escape) {window.close();                          //按esc键关闭窗口gameQuit = true;}startBtn.checkMouse(mousePosition, event);}
}

下面是实际效果

完善按钮类

上面的按钮只是一个简单的雏形,和我预想的稍微有些出入

所以做一下优化

我们给button增加几个属性

enum BtnState {NORMAL, HOVER, CLICK, RELEASE
};
int btnState;

写一个表示设定按钮状态的函数setState()

void Button::setState(int state) {btnState = state;switch (btnState) {case 0:setTexture(tNormal); break;case 1:setTexture(tHover); break;case 2:setTexture(tClick); break;case 3:setTexture(tNormal); break;default:break;}
}

修改一下checkMouse()函数(这里写的太绕圈子了,不知道当时怎么想的)

int Button::checkMouse(Vector2i mouse, Event event) {//判断鼠标是不是在按钮内,前提是放正的矩形,一般情况下都是这样,如果是奇形怪状的需要再写别的方法if ((mouse.x > getPosition().x && mouse.x < getPosition().x + getTexture()->getSize().x) &&(mouse.y > getPosition().y && mouse.y < getPosition().y + getTexture()->getSize().y)) {if (event.type == Event::EventType::MouseButtonPressed && event.mouseButton.button == Mouse::Left) {            //如果在范围里按下左键,一定是CLICK状态                                                                              //如果按下左建时setState(CLICK);} else if (event.type == Event::EventType::MouseButtonReleased && event.mouseButton.button == Mouse::Left) {   //如果范围内释放左键,if (btnState == CLICK) {                                                                                   //如果之前是CLICK状态,那么就是RELEASE状态,表示按下setState(RELEASE);                                                                                       //否则的话什么也不干}} else {                                                                                                        //如果鼠标移动的话,检测是不是按着的,不是就表示HOVER状态咯if (btnState != CLICK) {setState(HOVER);}}} else {                                                                                                          //鼠标在按钮范围外if (event.type == Event::EventType::MouseButtonReleased && event.mouseButton.button == Mouse::Left) {         //在范围外释放鼠标左键setState(NORMAL);                                                                                           //回归NORMAL状态} else if (btnState == HOVER) {                                                                                   //如果是HOVER,也就是也没有按下过,回归NORMAL状态setState(NORMAL);                                                                                          //其他就保持原样 比如按住不放的时候}}return btnState;                                                                                                   //最后返回按钮状态
}

这样差不多就能达到预期的效果了

然后我们其中一个按钮悬浮与按下的状态是相比原来高宽变大,所以为了保持按钮的位置看起来不那么奇怪,我们为其设置偏移量,然后再绘制

void Button::offset(double _x, double _y) {setPosition(getPosition().x + _x, getPosition().y + _y);
}

然后在Input()中调用

backToMenuBtn.offset(-5, -5);    //设定偏移量

看下效果

拆分Input()函数

之前我们只有一个场景,所以事件都写在一个Input里,现在我们多了一个场景,我们就需要startInput()以及fightInput()等等。

所以对Input部分作出优化,当场景不同时使用不同的Input

void Game::Input() {Event event;Vector2i mousePosition = Mouse::getPosition(window);while (window.pollEvent(event)) {               //接受事件if (event.type == Event::Closed) {window.close();                           //关闭键关闭窗口gameQuit = true;}if (event.type == sf::Event::EventType::KeyReleased && event.key.code == sf::Keyboard::Escape) {window.close();                          //按esc键关闭窗口gameQuit = true;}switch (gameSceneState) {case SCENE_START:startInput(mousePosition, event); break;case SCENE_FIGHT:fightInput(mousePosition, event); break;default:break;}}
}
void Game::startInput(Vector2i mousePosition, Event event) {}
void Game::fightInput(Vector2i mousePosition, Event event) {}

限制窗口大小

另外,在游玩过程中发现直接拉边框修改游戏窗口大小会导致按钮响应不了,把按钮的位置坐标改为百分比窗口大小也没用,推测是按钮绘制完后,窗口的大小改变会导致逻辑上的按钮的位置和画面上的按钮的位置不一样??

可以直接给定窗口大小,在绘制窗口时检测窗口大小是否符合规定的大小

void Game::Draw() {Vector2u size;size.x = windowWidth;size.y = windowHeight;window.setSize(size);
}

我们后面也可能涉及到游戏分辨率的修改,这样正好就可以派上用场了(又立flag,无了)

场景的跳转

分别在两个Input中写对应事件即可

void Game::startInput(Vector2i mousePosition, Event event) {backToMenuBtn.setState(0);if (startBtn.checkMouse(mousePosition, event) == 3) {gameSceneState = SCENE_FIGHT;loadMusic();}
}
void Game::fightInput(Vector2i mousePosition, Event event) {startBtn.setState(0);switch (backToMenuBtn.btnState) {case 1:case 2:backToMenuBtn.offset(-5, -5);   //设定偏移量default:break;}if (backToMenuBtn.checkMouse(mousePosition, event) == 3) {gameSceneState = SCENE_START;loadMusic();}
}

背景音乐管理

我们目前有两个背景音乐,当切换场景时就播放对应场景的音乐

音乐由两种变量来控制:一是音乐开关,我们之后会制作音乐开关的按钮,二是场景的状态

所以我们这么写音频加载的函数

void Game::loadMusic() {gameStartMusic.setLoop(true);            //背景音乐循环fightMusic.setLoop(true);switch (gameSceneState) {case SCENE_START:fightMusic.stop();if (startMusicState) {         //音乐开关gameStartMusic.play();} else {gameStartMusic.stop();}break;case SCENE_FIGHT:gameStartMusic.stop();if (fightMusicState) {          //音乐开关fightMusic.play();} else {fightMusic.stop();}break;default:break;}
}

成果

gif只有画面没有声音,自己想像一下吧

4月10日

功能实现

  1. 卡牌类实现
  2. 玩家类实现
  3. 绘制卡牌
  4. 抽牌

卡牌类

我们定义一个卡牌类,让它继承自按钮类,因为仔细想想,卡牌其实就类似于可拖拽的按钮。

(当时年轻什么也不会,这个卡牌类的逻辑我感觉写的很蠢…给自己挖了不少坑,因为一开始不知道vector的存在,也没想到用链表…)

在 Card.h 中把我们能想到的之后会用到的属性都写出来,值得注意的是,卡牌的类型定义了一个null,这是因为我们玩家类有手牌的属性,我们把初始手牌都设定为null的状态

#pragma once
#include "Button.h"
class Card : public Button {public:enum e_cardType {attack, mp, magic, movePower, fightBack, equip, null //红色攻击牌,法力牌,咒术牌,行动力牌,反制牌,装备牌,空};enum e_cardState {cardPool, handPool, disCardPool, noPool           //卡牌的状态,卡池,手牌区,弃牌区,被移除};int cost;           //消耗点数int damage;           //物理伤害int getMp;            //获得魔法int reduceMagic;  //消耗魔法int getMovePower; //获得行动力int getHp;           //加血int superHp;        //血量上限bool destory;     //是否“移除”卡牌bool getCard;     //抽牌int getCardNum;     //抽牌数int cardType;      //卡牌种类int cardState;        //卡牌状态bool discard;     //是否可弃牌Card();
};

在 Card.cpp 中完成构造函数

#include "Card.h"
Card::Card() {cardState = 0;       //默认状态为在卡池中destory = false;    //是否销毁(放入移除区)discard = true;     //是否放入弃牌区
}

玩家类

首先是Player.h

跟卡牌类一样,把我们暂时能想到的都先写上去~

这里我们定义一个表示手牌的指针数组,直接指向我们加载的卡牌们,对它们的属性进行修改来达到“手牌”的效果(回想起了被指针支配的恐惧)

#pragma once
#include "Card.h"
class Player {public:Player();int handMaxNum = 8;Card *handCards[8];             //手牌[数量上限]int handCardNums;             //手牌数目int getCardNum;                   //抽牌数目void getCard(Card[], int);            //抽牌void disCard();                 //弃牌bool getCardState;                  //玩家可否抽牌
};

然后完善Player.cpp

#include "Player.h"
Player::Player() {getCardState = true;                     //可否抽牌handCardNums = 0;                            //目前手牌数目getCardNum = 5;                                //抽牌数目
}
int getRandom(int min, int max) {                   //获取从min到max的随机数int random = rand() % (max - min + 1) + min;return random;
}
void Player::getCard(Card card[], int cardsLength) {for (int i = handCardNums; i < handCardNums + getCardNum; i++) {     //从 handCards[手牌数] 到 handCards[手牌数+抽牌数]给handCards赋值int index = getRandom(0, cardsLength);                             //获取范围(0,卡牌数量)的随机数if (card[index].cardState == 0) {                                  //如果卡牌在卡池里card[index].cardState = 1;                                       //卡牌移到手牌区handCards[handCardNums] = &card[index];                           //手牌数组赋值handCardNums += 1;                                                //手牌数加一}}
}

然后在Game.h中定义我们的贴图啊,卡牌啊,玩家啊啥的

这里我们先加载三张卡牌试下水

Card cards[3], nullCard;                                     //卡牌数组,“空“卡牌
Texture tCard1[3][100];                                         //卡牌纹理数组
Player humanPlayer, aiPlayer;                                   //人类玩家与ai玩家

然后是Game.cpp,先暂时这么写,之后我们会优化代码

抽牌的实现

加载卡片

void Game::Initial() {nullCard.cardState = nullCard.null;for (int i = 0; i < humanPlayer.handMaxNum; i++) {       //填充手牌数组humanPlayer.handCards[i] = &nullCard;}
}
void Game::loadMediaData() {///loadCards();
}
void Game::loadCards() {tCard1[0][0].loadFromFile("data/card1/card0.png");tCard1[1][0].loadFromFile("data/card1/card1.png");tCard1[2][0].loadFromFile("data/card1/card2.png");tCard1[0][1].loadFromFile("data/card1/card3.png");tCard1[1][1].loadFromFile("data/card1/card4.png");tCard1[2][1].loadFromFile("data/card1/card5.png");tCard1[0][2].loadFromFile("data/card1/card6.png");tCard1[1][2].loadFromFile("data/card1/card7.png");tCard1[2][2].loadFromFile("data/card1/card8.png");cards[0].setTextures(tCard1[0][0], tCard1[1][0], tCard1[2][0]);cards[1].setTextures(tCard1[0][1], tCard1[1][1], tCard1[2][1]);cards[2].setTextures(tCard1[0][2], tCard1[1][2], tCard1[2][2]);
}

交互事件

void Game::fightInput(Vector2i mousePosition, Event event) {for (int i = 0; i <= humanPlayer.handCardNums; i++) {if (humanPlayer.handCards[i]->cardState != humanPlayer.handCards[i]->null) {humanPlayer.handCards[i]->checkMouse(mousePosition, event);        //卡片事件}}
}

绘制卡牌

void Game::drawFight() {                 //战斗场景drawCard();
}
void Game::drawCard() {cards[0].setPosition(600, 500);cards[1].setPosition(700, 500);cards[2].setPosition(800, 500);if (humanPlayer.getCardState) {humanPlayer.getCard(cards, sizeof(cards) / sizeof(cards[0]));                //抽牌  获取数组大小humanPlayer.getCardState = false;}window.draw(*humanPlayer.handCards[0]);      //绘制手牌window.draw(*humanPlayer.handCards[1]);       //绘制手牌window.draw(*humanPlayer.handCards[2]);       //绘制手牌window.display();                     //展示屏幕
}

看下效果

好像出了点问题,怎么切换到战斗场景时卡牌还绘制的是退出战斗场景时的普通的状态

这里我找了将近4个 小时的bug,一直到凌晨4点,终于以为找到问题了,在某个地方加了一行代码后,试了几次可以正常绘制了,第二天再一测试,发现昨天其实是洗牌洗的和上次一样。。。(太真实了)

后来终于知道怎么解决了

在抽牌后立即设定卡牌贴图是普通状态

void Game::Draw() {switch (gameSceneState) {     //场景判断case SCENE_FIGHT:if (humanPlayer.getCardState) {humanPlayer.getCard(cards, sizeof(cards) / sizeof(cards[0]));             //抽牌  获取数组大小humanPlayer.getCardState = false;humanPlayer.handCards[0]->setState(0);     //设置贴图为普通状态humanPlayer.handCards[1]->setState(0);humanPlayer.handCards[2]->setState(0);}drawFight();break;default:break;}
}

最后是效果,已经可以实现随机抽牌了

4月11日

功能实现

  1. 完善卡牌绘制

  2. 完善随机抽牌

卡牌加载

我们使用循环将文件夹里的图片依次加载到纹理之中,因为素材有点多,所以我们加载需要一些时间,我们先加载十张,看看是不是从十张的卡池中随机抽牌

void Game::loadCards() {stringstream ssNormalCard;stringstream ssHoverCard;stringstream ssClickCard;for (int j = 0; j < 10; j++) {ssNormalCard << "data/card1/card" << j << ".png";tCard1[0][j].loadFromFile(ssNormalCard.str());ssHoverCard << "data/hoverCard1/card" << j << ".png";tCard1[1][j].loadFromFile(ssHoverCard.str());ssClickCard << "data/clickCard1/card" << j << ".png";tCard1[2][j].loadFromFile(ssClickCard.str());ssNormalCard.str("");ssHoverCard.str("");ssClickCard.str("");cards[j].setTextures(tCard1[0][j], tCard1[1][j], tCard1[2][j]);}
}

在input中绑定鼠标事件

 for (int i = 0; i < humanPlayer.handCardNums; i++) {if (humanPlayer.handCards[i]->cardState != humanPlayer.handCards[i]->null) {humanPlayer.handCards[i]->checkMouse(mousePosition, event);     //卡片事件}}

卡牌绘制

绘制也一样,写成循环

void Game::Draw() {switch (gameSceneState) {     //场景判断case SCENE_START:drawStart();break;case SCENE_FIGHT:if (humanPlayer.getCardState) {humanPlayer.getCard(cards, humanPlayer.getCardNum, sizeof(cards) / sizeof(cards[0]));                  //抽牌  获取抽牌数量for (int i = 0; i < humanPlayer.handCardNums; i++) {humanPlayer.handCards[i]->setState(0);     //设置贴图为普通状态}humanPlayer.getCardState = false;}drawFight();break;default:break;}
}
void Game::drawCard() {for (int i = 0; i < humanPlayer.handCardNums; i++) {humanPlayer.handCards[i]->setPosition(200 + i * 200, 600);window.draw(*humanPlayer.handCards[i]);}window.display();                        //展示屏幕
}

随机抽牌

我们重写之前写的抽牌函数,因为有一些bug

洗牌

void Player::randCardPool(Card card[], int cardsLength) {srand(time(0));for (int j = 0; j < cardsLength; j++) {int index = rand() % cardsLength;if (index != j) {Card temp = card[j];card[j] = card[index];card[index] = temp;}                                       //洗牌}
}

抽牌

void Player::getCard(Card card[], int getNum, int cardLength) {randCardPool(card, cardLength);for (int i = 0; i < getNum; i++) {       //从 handCards[手牌数] 到 handCards[手牌数+抽牌数]给handCards赋值for (int k = 0; k < cardLength; k++) {if (card[k].cardState == card[k].cardPool) {                                  //如果卡牌在卡池里就抽牌card[k].cardState = 1;                                        //卡牌移到手牌区handCardNums += 1;                                               //手牌数加一handCards[handCardNums - 1 + i] = &card[k];                            //手牌数组赋值getNum--;                       //抽牌数-1}if (!getNum) {                  //抽牌数减为0return;}}}
}

最后来看一下效果

4月13日

功能实现

  1. 游戏加载场景
  2. 精灵动画
  3. 基本的角色UI

游戏加载场景

上一篇中我们说到游戏素材有点多,加载起来需要一定时间,所以我们这次专门创建一个线程用来进行游戏的加载

设定初始场景

Game::Game() {//gameSceneState = SCENE_LOADING;                 //初始默认游戏场景initMusic();                                  //初始化音乐
}

音乐初始化

void Game::initMusic() {gameStartMusic.setLoop(true);            背景音乐循环fightMusic.setLoop(true);gameStartMusic.play();
}

把加载素材的函数都写到一个里面

void Game::loadMediaData() {//loadCards();loadMusic();gameSceneState = SCENE_START;
}

加载场景的绘制

void Game::drawLoading() {Texture tBk, loads;stringstream ss;tBk.loadFromFile("data/bg/loading.png");Sprite sBk, sLoad;sBk.setTexture(tBk);sBk.setPosition(0, 0);window.draw(sBk);window.display();
}

主函数中

int main() {Game game;thread thLoad(&Game::loadMediaData, &game);            //创建一个线程加载游戏素材thLoad.detach();                                  //独立于主线程运行game.Run();return 0;
}

这样就会先加载游戏加载场景,等素材加载完后再进入游戏,免得素材比较多时造成窗口白屏以及无法操作的情况

加载动画

我们来完善一下加载界面,做一个加载的动画

像这样,准备一些用来做序列帧的图片(手动K帧…)

接下来我们定义一个Animation类,继承自Sprite

Animation.h

#pragma once
#include <SFML/Graphics.hpp>
using namespace sf;
class Animation :public Sprite {public:Texture* frames;                 //纹理序列int frameLength;                  //序列量int frameNo = 0;                  //当前播放帧void bindFrames(Texture[], int);             //绑定序列void play();          //播放动画
};

Animation.cpp

#include "Animation.h"void Animation::bindFrames(Texture tFrames[], int length) {      //传入纹理数组,数组长度frames = new Texture[length];                              //让内置的纹理数组等于for (int i = 0; i < length; i++) {                            //给动态定义长度frames[i] = tFrames[i];}frameLength = length;
}
void Animation::play() {setTexture(frames[frameNo]);            //设定当前帧的纹理frameNo += 1;                           //下一帧if (frameNo == frameLength) {            //重新开始播放frameNo = 0;}
}

初始化加载场景的数据

void Game::initLoading() {tLoadBk.loadFromFile("data/bg/loading.png");     //加载背景stringstream ss;for (int i = 0; i < 40; i++) {                      //加载动画序列ss << "data/anime/load" << i << ".png";tLoads[i].loadFromFile(ss.str());ss.str("");}sLoadBk.setTexture(tLoadBk);anLoading.bindFrames(tLoads, sizeof(tLoads) / sizeof(tLoads[0]));   //绑定动画序列anLoading.setScale(0.5, 0.5);                                       //缩放
}

绘制加载场景

void Game::drawLoading() {sLoadBk.setPosition(0, 0);window.draw(sLoadBk);anLoading.setPosition(800, 400);anLoading.play();                       //播放当前帧window.draw(anLoading);window.display();
}

最后在draw()函数中判断场景绘制即可

下面来看下效果

玩家,敌人的绘制

这里就比较简单了,就是单纯的绘制贴图

难点是hp条,mp条,行动力,卡池剩余牌数的改变,这些我们之后再做打算

为了还原真实的手牌,我们绘制手牌时对其设置偏移量,让其交叉绘制(这里又伏笔了,处处给自己挖坑)

void Game::drawCard() {for (int i = 0; i < humanPlayer.handCardNums; i++) {humanPlayer.handCards[i]->setPosition(300 + i * 150, 500);window.draw(*humanPlayer.handCards[i]);}window.display();                     //展示屏幕
}

以下是目前的效果

4月14日

功能实现

  1. 卡牌交互音效

  2. 卡牌拖拽

卡牌交互音效

在之前我们已经定义过SoundBuffer对象以及Sound对象,接下来对其进行绑定

void Game::loadMediaData() {//加载音频victorSb.loadFromFile("data/sound/victor.ogg");defeatSb.loadFromFile("data/sound/defeat.ogg");attackSb.loadFromFile("data/sound/attack.ogg");hoverSb.loadFromFile("data/sound/hover.ogg");pressSb.loadFromFile("data/sound/press.ogg");releaseSb.loadFromFile("data/sound/release.ogg");victorSd.setBuffer(victorSb);defeatSd.setBuffer(defeatSb);attackSd.setBuffer(attackSb);hoverSd.setBuffer(hoverSb);pressSd.setBuffer(pressSb);releaseSd.setBuffer(releaseSb);
}

然后我们再Button类中添加多个属性,方便我们对音效进行管理

因为我们是多张卡牌操作同一个音效

而且我们的卡牌有三种音效,其中释放时的音效是一定会播放的,所以不用管理

悬浮或按下的音效都是在“鼠标在卡牌上”的前提下,需要用bool变量来判断可否播放

class Button :public Sprite {   //继承SFML的Sprite类
public:bool hoverSd;                //是否播放音效 悬浮时的音效bool pressSd;                //按下时的音效
};

然后在战斗场景的交互函数中

void Game::fightInput(Vector2i mousePosition, Event event) {for (int i = 0; i < humanPlayer.handCardNums; i++) {if (humanPlayer.handCards[i]->cardState != humanPlayer.handCards[i]->null) {switch (humanPlayer.handCards[i]->checkMouse(mousePosition, event)) {        //卡片事件case 0:humanPlayer.handCards[i]->hoverSd = true;                  //如果卡片为普通状态 比如 鼠标移到卡牌外 原地释放卡牌humanPlayer.handCards[i]->pressSd = true;if (humanPlayer.handCards[i]->mouseContain(mousePosition)) {           //如果鼠标在卡牌上} else if (mousePosition.x > (300 + (humanPlayer.handCardNums - 1) * 150 + 200) || mousePosition.x < 300 || mousePosition.y>800 || mousePosition.y < 500) {     //鼠标在手牌区外pressSd.stop();                                            //按下时的音效hoverSd.stop();                                         //悬浮时的音效}break;case 1:pressSd.stop();if (humanPlayer.handCards[i]->hoverSd) {                    //如果悬浮音效可播放hoverSd.play();                                          //播放悬浮音效humanPlayer.handCards[i]->hoverSd = false;              //不可播放悬浮音效}break;case 2:hoverSd.stop();if (humanPlayer.handCards[i]->pressSd) {pressSd.play();humanPlayer.handCards[i]->pressSd = false;}break;case 3:pressSd.stop();releaseSd.play();humanPlayer.handCards[i]->hoverSd = true;                  //释放卡牌,音效可播放humanPlayer.handCards[i]->pressSd = true;break;default:break;}}}
}

但是我们发现,当鼠标移到上面卡牌和下面卡牌重叠的部分时,两张卡牌都会交互,所以我们重载按钮类的鼠标检测函数,只需要为其设定两个偏移量即可(伏笔回收)

button.h

bool mouseContain(Vector2i, int, int);                       //检测鼠标是否在精灵内
int checkMouse(Vector2i, Event, int, int);                  //检查鼠标状态

button.cpp

bool Button::mouseContain(Vector2i mouse, int xOffset, int yOffset) {return (mouse.x > getPosition().x && mouse.x < getPosition().x + getTexture()->getSize().x - xOffset) &&(mouse.y > getPosition().y && mouse.y < getPosition().y + getTexture()->getSize().y - yOffset) ? true : false;
}
int Button::checkMouse(Vector2i mouse, Event event, int xOffset, int yOffset) {//判断鼠标是不是在按钮内,前提是放正的矩形,一般情况下都是这样,如果是奇形怪状的需要再写别的方法if (mouseContain(mouse, xOffset, yOffset)) {}
}

最后稍微修改下之前的交互函数

for (int i = 0; i < humanPlayer.handCardNums; i++) {if (humanPlayer.handCards[i]->cardState != humanPlayer.handCards[i]->null) {if (i != humanPlayer.handCardNums - 1) {   //如果不是最后一张手牌(在最上面)cardOffset.x = 50;cardOffset.y = 0;} else {cardOffset.x = 0;cardOffset.y = 0;}switch (humanPlayer.handCards[i]->checkMouse(mousePosition, event, cardOffset.x, cardOffset.y)) {      //卡片事件}}
}

看一下现在的效果

卡牌拖拽

基本原理就是在卡牌按下前记录鼠标的初始位置以及卡牌初始位置,然后每帧判断鼠标初始与现在位置的坐标差(偏移量),在给卡牌设置初始位置加上偏移量即可

在Player.h中设定一个属性

int cardSelect;                  //与哪个卡牌正在交互

Card.h中定义一些属性

void move(Vector2i); //卡牌移动
Vector2f originPosition;    //初始位置
Vector2i originMouse;       //初始鼠标位置

卡牌移动

void Card::move(Vector2i mouse) {setPosition(originPosition.x + mouse.x - originMouse.x, originPosition.y + mouse.y - originMouse.y);
}

Game.cpp中

卡牌绘制

void Game::drawCard() {for (int i = 0; i < humanPlayer.handCardNums; i++) {if (humanPlayer.handCards[i]->btnState == humanPlayer.handCards[i]->CLICK) {                //如果为按下的状态humanPlayer.handCards[i]->move(Mouse::getPosition(window));                                //卡牌移动事件} else {humanPlayer.handCards[i]->setPosition(300 + i * 150, 500);                              //默认位置humanPlayer.handCards[i]->originPosition = humanPlayer.handCards[i]->getPosition();        //设置默认位置humanPlayer.handCards[i]->originMouse = Mouse::getPosition(window);                     //设定按下鼠标前鼠标的位置}window.draw(*humanPlayer.handCards[i]);}window.display();                        //展示屏幕
}

优化一下战斗场景的交互

void Game::fightInput(Vector2i mousePosition, Event event) {for (int i = 0; i < humanPlayer.handCardNums; i++) {if (humanPlayer.handCards[i]->cardState != humanPlayer.handCards[i]->null) {if (humanPlayer.handCards[i]->mouseContain(mousePosition, cardOffset.x, cardOffset.y)) {humanPlayer.cardSelect = i;                             //与第i张卡牌进行交互}switch (humanPlayer.handCards[i]->checkMouse(mousePosition, event, cardOffset.x, cardOffset.y)) {       //卡片事件case 0:humanPlayer.handCards[i]->hoverSd = true;                  //如果卡片为普通状态 比如 鼠标移到卡牌外 原地释放卡牌humanPlayer.handCards[i]->pressSd = true;if (humanPlayer.cardSelect != -1) {                          //如果鼠标在与卡牌进行交互} else if (mousePosition.x > (300 + (humanPlayer.handCardNums - 1) * 150 + 200) || mousePosition.x < 300 || mousePosition.y>800 || mousePosition.y < 500) {     //鼠标在手牌区外pressSd.stop();                                            //按下时的音效hoverSd.stop();                                         //悬浮时的音效}break;case 1://case 2://case 3:pressSd.stop();releaseSd.play();humanPlayer.handCards[i]->setState(humanPlayer.handCards[i]->NORMAL);     //释放后设定为普通状态humanPlayer.cardSelect = -1;                               //没有在与卡牌进行交互humanPlayer.handCards[i]->hoverSd = true;                   //释放卡牌,音效可播放humanPlayer.handCards[i]->pressSd = true;break;default:break;}}}
}

最后来看下效果

4月15日

功能实现

  1. 敌人绘制

  2. 属性绘制

  3. 出牌

敌人绘制

我们自定义一个敌人类,继承自玩家类

Enemy.h

#pragma once
#include "Player.h"using namespace sf;
class Enemy :public Player {public:int name;                        //名字Texture tShape;                 //敌人纹理Sprite shape;                 //敌人精灵void setPosition(Vector2f);       //设定绘制位置void setName(int);              //设定名字
};

Enemy.cpp

#include "Enemy.h"
void Enemy::setName(int num) {name = num;switch (name) {case carpente:tShape.loadFromFile("./data/enemy/carpente.png");shape.setTexture(tShape);break;case prisone:tShape.loadFromFile("./data/enemy/prisone.png");shape.setTexture(tShape);break;default:break;}
}void Enemy::setPosition(Vector2f pos) {shape.setPosition(pos);
}

这样在实例化敌人对象的时候name不同加载不同的贴图(最后也没用上)

然后Game.cpp中

void Game::drawEnemy() {enemyCarpente.shape.setPosition(680, 10);window.draw(enemyCarpente.shape);sEnemyUI.setPosition(635, 300);window.draw(sEnemyUI);
}

最后在drawFight中调用

效果如下

属性绘制

接下来进行属性的绘制

在Player.h中添加一些属性

class Player {public:Texture tNums[10];           //数字纹理int hp;                       //血量int fullHp;                 //满血血量int mp;                       //蓝量int movePower;              //行动力void drawState(RenderWindow*);         //更新状态int cardPoolNum;          //卡池中卡牌数量Sprite sMovePower;         //行动力Texture tHpBar;                //血量纹理Texture tMpBar;               //蓝量Sprite hpNum[2], hpBar;     //血量精灵Sprite mpNum[2], mpBar;       //蓝量void loadMedia();Player();
};

在Player.cpp中完善

Player::Player() {getCardState = true;                      //可否抽牌handCardNums = 0;                            //目前手牌数目getCardNum = 5;                                //抽牌数目cardSelect = -1;                         //没有进行卡牌交互fullHp = 20;hp = fullHp;mp = 5;movePower = 1;nullCard.cardState = nullCard.null;for (int i = 0; i < handMaxNum; i++) {     //填充手牌数组handCards[i] = &nullCard;}hpNum[0].setScale(0.7, 0.7);hpNum[1].setScale(0.7, 0.7);mpNum[0].setScale(0.7, 0.7);mpNum[1].setScale(0.7, 0.7);hpNum[0].setPosition(640, 868);hpNum[1].setPosition(620, 868);mpNum[0].setPosition(920, 868);mpNum[1].setPosition(900, 869);hpBar.setPosition(780, 870);mpBar.setPosition(1028, 870);sMovePower.setPosition(1080, 860);
}
void Player::loadMedia() {tHpBar.loadFromFile("./data/ui/kong.png");  //设定贴图tMpBar.loadFromFile("./data/ui/skong.png");for (int i = 0; i < 10; i++) {stringstream ss;ss << "./data/ui/" << i << ".png";tNums[i].loadFromFile(ss.str());}hpBar.setTexture(tHpBar);mpBar.setTexture(tMpBar);
}
void Player::drawState(RenderWindow* window) {//遮挡条在右上角mp <= 10 ? mpBar.setScale(-1 * (1 - mp / (float)10), 1) : mpBar.setScale(0, 1);          //mp增加,遮挡条变短hpBar.setScale(-1 * (1 - hp / (float)fullHp), 1);window->draw(mpBar);window->draw(hpBar);if (hp > 10) {hpNum[1].setTexture(tNums[hp / 10]);window->draw(hpNum[1]);}hpNum[0].setTexture(tNums[hp % 10]);window->draw(hpNum[0]);if (mp > 10) {mpNum[1].setTexture(tNums[mp / 10]);window->draw(mpNum[1]);}mpNum[0].setTexture(tNums[mp % 10]);window->draw(mpNum[0]);}

Enemy类中增加

class Enemy :public Player {public:void loadMedia();Enemy();
};

Enemy.cpp

#include "Enemy.h"
#include <iostream>
#include <sstream>
using namespace std;
Enemy::Enemy() {hpNum[0].setScale(0.7, 0.7);hpNum[1].setScale(0.7, 0.7);mpNum[0].setScale(0.7, 0.7);mpNum[1].setScale(0.7, 0.7);hpNum[0].setPosition(800, 330);hpNum[1].setPosition(780, 330);mpNum[0].setPosition(800, 350);mpNum[1].setPosition(780, 350);hpBar.setPosition(913, 334);mpBar.setPosition(867, 354);sMovePower.setScale(0.8, 0.8);sMovePower.setPosition(888, 346);
}
void Enemy::loadMedia() {tHpBar.loadFromFile("./data/ui/blong.png");  //设定贴图tMpBar.loadFromFile("./data/ui/bshort.png");for (int i = 0; i < 10; i++) {stringstream ss;ss << "./data/ui/" << i << ".png";tNums[i].loadFromFile(ss.str());}hpBar.setTexture(tHpBar);mpBar.setTexture(tMpBar);sMovePower.setTexture(tNums[1]);
}

最后在Game.cpp中调用

void Game::drawPlayer() {sPlayUI.setPosition(0, 700);window.draw(sPlayUI);humanPlayer.drawState(&window);
}
void Game::drawEnemy() {enemyCarpente.shape.setPosition(680, 10);window.draw(enemyCarpente.shape);sEnemyUI.setPosition(635, 300);window.draw(sEnemyUI);enemyCarpente.drawState(&window);
}

出牌

给敌人增加一个检测鼠标位置的函数

bool Enemy::checkMouse(Vector2f mouse) {return ((mouse.x > shape.getPosition().x && mouse.x < shape.getPosition().x + shape.getTexture()->getSize().x) &&(mouse.y > shape.getPosition().y && mouse.y < shape.getPosition().y + shape.getTexture()->getSize().y)) ? true : false;
}

出牌函数

void Player::useCardTo(Card* card, Player* player) {card->cardState = card->disCardPool;handCardNums--;player->hp--;
}

在交互事件中

for (int i = 0; i < humanPlayer.handCardNums; i++) {switch (humanPlayer.handCards[i]->checkMouse(mousePosition, event, cardOffset.x, cardOffset.y)) {       //卡片事件case 3:if (enemyCarpente.checkMouse(mousePosition)) {attackSd.play();                                                 //播放攻击音乐humanPlayer.useCardTo(humanPlayer.handCards[i], &enemyCarpente);    //对敌人使用卡牌humanPlayer.handCards[i] = &humanPlayer.nullCard;                 //设定使用后的手牌为null} else {humanPlayer.handCards[i]->setState(humanPlayer.handCards[i]->NORMAL);      //释放后设定为普通状态}break;}
}

目前为止的效果

4月16日

功能实现

  1. 完善卡牌绘制

  2. 回合结束

  3. 弃牌

完善卡牌绘制

之前的卡牌绘制位置基本上是固定的,我想每出一张牌位置就更新一下,所以修改下卡牌绘制的函数

void Game::drawCard() {int pos = 0;                 //记录绘制位置for (int i = 0; i < humanPlayer.handMaxNum; i++) {if (humanPlayer.handCards[i]->cardState != humanPlayer.handCards[i]->null) {//cout << "is " << i << endl;if (humanPlayer.handCards[i]->btnState == humanPlayer.handCards[i]->CLICK) {                //如果为按下的状态humanPlayer.handCards[i]->move(Mouse::getPosition(window));                                //卡牌移动事件} else {humanPlayer.handCards[i]->setPosition((float)(300 + pos * 150), (float)500);                                //默认位置humanPlayer.handCards[i]->originPosition = humanPlayer.handCards[i]->getPosition();        //设置默认位置humanPlayer.handCards[i]->originMouse = Mouse::getPosition(window);                     //设定按下鼠标前鼠标的位置}window.draw(*humanPlayer.handCards[i]);if (pos < humanPlayer.handCardNums) {pos++;}}}window.display();                      //展示屏幕
}

回合结束

首先重写一下之前的抽牌函数,今天又出现了许多bug。。

void Player::getCard(Card card[], int getNum, int cardLength) {if (handCardNums == handMaxNum) {return;} else if (getNum + handCardNums > handMaxNum) {getNum = handMaxNum - handCardNums;}srand((unsigned)time(NULL));if (handCardNums <= handMaxNum) {for (; getNum != 0;) {int num = rand() % cardLength;for (int i = 0; i < handMaxNum; i++) {if (handCards[i] == &nullCard) {if (card[num].cardState == card[num].cardPool) {                                    //如果卡牌在卡池里就抽牌card[num].cardState = card[num].handPool;                                     //卡牌移到手牌区handCardNums += 1;                                               //手牌数加一handCards[i] = &card[num];                          //手牌数组赋值getNum--;}}}}}
}

Game.cpp中添加回合结束按钮的事件

void Game::fightInput(Vector2i mousePosition, Event event) {switch (turnEnd.checkMouse(mousePosition, event, 0, 0)) {case 1:hoverSd.play();break;case 3:releaseSd.play();humanPlayer.getCard(cards, humanPlayer.getCardNum, sizeof(cards) / sizeof(cards[0]));break;default:break;}
}

效果如下

弃牌

原版的弃牌是回合结束时手牌数大于手牌上限才要求弃牌,为了简化,我直接设置一个主动弃牌的功能,正好放在之前被我阉割的技能的位置

所以要准备两张图片,普通的删除和长按图片高亮的删除

在Player.cpp中添加弃牌动作

void Player::disCard(Card* card) {                       //弃牌card->cardState = card->disCardPool;         //放入弃牌堆handCardNums--;                              //手牌数减一
}

然后在之前出牌的地方改一下

if (enemyCarpente.checkMouse(mousePosition)) {attackSd.play();                                                   //播放攻击音乐humanPlayer.useCardTo(humanPlayer.handCards[i], &enemyCarpente);    //对敌人使用卡牌humanPlayer.handCards[i] = &humanPlayer.nullCard;                 //设定使用后的手牌为null} else {if (humanPlayer.handCards[i]->mouseContainf(disCardBtn.getPosition(), 30, -30)) {humanPlayer.disCard(humanPlayer.handCards[i]);humanPlayer.handCards[i] = &humanPlayer.nullCard;                 //设定使用后的手牌为null} elsehumanPlayer.handCards[i]->setState(humanPlayer.handCards[i]->NORMAL);        //释放后设定为普通状态}

这样讲鼠标拖拽到删除的按钮上就可以实现弃牌了

但是当按钮比较小时根据鼠标判断就很难受,所以我们根据按钮的坐标是不是进入卡牌坐标的范围来判断

if (humanPlayer.handCards[i]->mouseContain(disCardBtn.getPosition(), 30, -30)) {humanPlayer.disCard(humanPlayer.handCards[i]);humanPlayer.handCards[i] = &humanPlayer.nullCard;                  //设定使用后的手牌为null}

看下效果

4月17日

功能实现

  1. 对话框绘制
  2. 敌人的出牌

对话框

在战斗界面按下右上角返回按钮时我们增加一个对话框用来提示进一步操作,避免误触导致gg

在Game.cpp中写绘制对话框的函数

void Game::drawPlayer() {window.draw(sPlayUI);humanPlayer.drawState(&window);window.draw(turnEnd);window.draw(disCardBtn);
}

然后设置一个默认为false的变量showBackDialog,在需要对话框出现的地方进行判断

void Game::drawFight() {                 //战斗场景window.clear();                           //清屏window.draw(sGameFightBK);              //绘制背景图backToMenuBtn.setPosition(windowWidth * 0.9f, windowHeight * 0.05f); //按百分比绘制    不要写到别的函数里去window.draw(backToMenuBtn);drawPlayer();drawEnemy();//绘制drawCard();if (showBackDialog) {drawDialog();}window.display();                       //展示屏幕
}

在交互函数中修改

switch (backToMenuBtn.checkMouse(mousePosition, event)) {case 1:break;case 3:releaseSd.play();                                       //释放按钮的声音showBackDialog = true;                                    //显示对话框break;}if (showBackDialog) {switch (yesBtn.checkMouse(mousePosition, event)) {case 3:gameSceneState = SCENE_START;                          //切换场景loadMusic();                                          //加载音乐showBackDialog = false;                              //不显示对话框yesBtn.setState(0);                                 //设定按钮为普通状态break;}switch (noBtn.checkMouse(mousePosition, event)) {case 3:showBackDialog = false;break;}}

下面是效果

敌人的出牌

首先在Enemy类中定义一些必要的属性

void getCard(Card[], int, int);          //抽牌
void showCard(RenderWindow*);       //把敌人用过的卡牌展示在屏幕上
Card cardShow;              //展示的卡牌
Vector2f cardPosition;      //敌人卡牌的绘制位置
Clock useCardTimer;         //出牌定时器

在抽牌时启动计时器,让敌人的每张出牌之间有一定的间隔

void Enemy::getCard(Card cards[], int getNum, int length) {Player::getCard(cards, getNum, length);cardShow = nullCard;          //回合开始时出牌为nulluseCardTimer.restart();           //启动计时器
}

然后是绘制卡牌

void Enemy::showCard(RenderWindow* window) {if (handCardNums >= 0) {if (useCardTimer.getElapsedTime().asMilliseconds() > 1200) {      //大于1200 msfor (int i = 0; i < handMaxNum; i++) {if (handCards[i]->cardState != handCards[i]->null) {      //如果手牌不为空cardShow = *handCards[i];                             //展示该卡牌useCardTimer.restart();                                  //重启计时器disCard(handCards[i]);                                   //弃牌handCards[i] = &nullCard;                              //手牌数组位置为nullreturn;                                                    //退出循环}}} else {if (cardShow.cardState != null) {              //展示的卡牌状态不为nullwindow->draw(cardShow);                       //绘制卡牌}}}
}

然后按回合按钮时转到敌人回合

void Game::fightInput(Vector2i mousePosition, Event event) {switch (turnEnd.checkMouse(mousePosition, event, 0, 0)) {case 1:hoverSd.play();break;case 3:releaseSd.play();enemyCarpente.getCard(cards, enemyCarpente.getCardNum, sizeof(cards) / sizeof(cards[0]));break;}
}

最后进行出牌的 绘制

void Game::drawEnemy() {window.draw(enemyCarpente.shape);window.draw(sEnemyUI);enemyCarpente.drawState(&window);enemyCarpente.showCard(&window);     //出牌
}

效果如下

卡牌交互Bug修复

由于修改了手牌数组运作的相关方法,之前卡牌交互的部分出现了bug:有时候不能正确判断最右边的手牌是视觉上的最右边的手牌(回顾到这我已经晕了)

在交互函数中修改

int cardPos = 0;                                            //当前卡牌是手牌中的第几张
for (int i = 0; i < humanPlayer.handMaxNum; i++) {if (humanPlayer.handCards[i]->cardState != humanPlayer.handCards[i]->null) {cardPos++;if (cardPos != humanPlayer.handCardNums) {  //如果不是最后一张手牌(在最上面)cardOffset.x = 80;cardOffset.y = 0;} else {cardOffset.x = 0;cardOffset.y = 0;}}
}

窗口的优化

之前的游戏窗口大小是可以被改变的,我们在创建窗口时增加窗口样式来限制窗口大小

Uint32 windowStyle = sf::Style::Close | sf::Style::Titlebar;
window.create(sf::VideoMode(windowWidth, windowHeight), "月圆之夜", windowStyle);

这样窗口大小就不会被改变了

4月18日

功能实现

  1. 轮流出牌
  2. 游戏结束(胜利,失败)

轮流出牌

在Game.h中定义变量来控制回合轮转

enum e_whosTurn {                    //谁的回合ePlayerTurn, eEnemyTurn
};
int whosTurn = 0;              //默认玩家回合

按下回合结束按钮地方抽牌,出牌

void Game::fightInput(Vector2i mousePosition, Event event) {switch (turnEnd.checkMouse(mousePosition, event, 0, 0)) {case 1:hoverSd.play();break;case 3:releaseSd.play();whosTurn = eEnemyTurn;                                 //敌人回合enemyCarpente.getCardState = true;                       //可摸牌enemyCarpente.useCardNum = enemyCarpente.getCardNum;  //设定可用牌数量turnEnd.setState(turnEnd.NORMAL);break;}
}

敌人出牌的绘制

void Game::drawEnemy() {if (whosTurn == eEnemyTurn && !gameLose) {enemyCarpente.showCard(&window, &humanPlayer);if (enemyCarpente.cardShow.cardState != enemyCarpente.cardShow.null) {enemyCarpente.cardShow.setPosition(enemyCarpente.cardPosition);window.draw(enemyCarpente.cardShow);}}
}

为了方便管理,我把这部分写在logic函数中

void Game::Logic() {switch (gameSceneState) {case SCENE_FIGHT:if (humanPlayer.hp <= 0) {gameLose = true;enemyCarpente.cardShow = enemyCarpente.nullCard;}if (enemyCarpente.hp <= 0) {gameWin = true;}if (whosTurn == eEnemyTurn) {if (enemyCarpente.getCardState) {enemyCarpente.getCard(enemyCards, enemyCarpente.getCardNum, sizeof(enemyCards) / sizeof(enemyCards[0]));             //抽牌  获取抽牌数量enemyCarpente.getCardState = false;}if (enemyCarpente.useCardNum == 1) {enemyCarpente.lastCardClock.restart();                                                   //启动计时器}if (enemyCarpente.useCardNum == 0) {if (enemyCarpente.lastCardClock.getElapsedTime().asMilliseconds() > 1600) {                //获取最后一张牌展示时间whosTurn = ePlayerTurn;                                                               //玩家回合humanPlayer.getCardState = true;                                                 //玩家可抽牌enemyCarpente.cardShow = enemyCarpente.nullCard;                                    //设定展示卡牌为null}}} else if (whosTurn == ePlayerTurn && !gameLose) {if (humanPlayer.getCardState) {humanPlayer.getCard(playerCards, humanPlayer.getCardNum, sizeof(playerCards) / sizeof(playerCards[0]));               //抽牌  获取抽牌数量humanPlayer.getCardState = false;for (int i = 0; i < humanPlayer.handCardNums; i++) {humanPlayer.handCards[i]->setState(0);       //设置贴图为普通状态}}}break;}
}

效果如下

游戏结束

首先肯定是Texture和Sprite以及Button初始化了~

做完这些后,我们进行游戏胜利与游戏失败画面的绘制

在对话框绘制的函数中增加判断

void Game::drawDialog() {if (showBackDialog) {window.draw(sDialog);window.draw(yesBtn);window.draw(noBtn);}if (gameLose) {window.draw(gmLoseDialog);window.draw(gmOverReplayBtn);window.draw(gmOvertoMenuBtn);}if (gameWin) {window.draw(gmWinDialog);}
}

在fightInput函数中增加失败或胜利界面绘制出来后的交互,此时只允许进行与失败/胜利对话框的交互

void Game::fightInput(Vector2i mousePosition, Event event) {if (gameWin) {if (event.type == Event::EventType::MouseButtonPressed && event.mouseButton.button == Mouse::Left) {gameSceneState = SCENE_START;             //切换场景gameWin = false;                         //}} else if (gameLose) {switch (gmOverReplayBtn.checkMouse(mousePosition, event)) {case 3:initFightData();gmOverReplayBtn.setState(gmOverReplayBtn.NORMAL);}switch (gmOvertoMenuBtn.checkMouse(mousePosition, event)) {case 3:gameLose = false;gameSceneState = SCENE_START;                                 //切换场景gmOvertoMenuBtn.setState(gmOvertoMenuBtn.NORMAL);             //设定为普通状态loadMusic();                                                   //切换音乐}} else{///}
}

效果如下

Bug修复

当敌方出牌时我们不能出牌,所以要在玩家出牌的所有事件处理加上一个前提条件

回合结束按钮的处理也一样放到这个判断里

if (whosTurn == ePlayerTurn) {///
}

当敌人还没出完牌我们就死掉的话,虽然会弹出失败的对话框,但是此时判定敌人已经出完牌了,又到了我们的回合,又会继续抽牌,在logic中给玩家判断一下是否已经gamelose

if (whosTurn == ePlayerTurn && !gameLose) {if (humanPlayer.getCardState) {humanPlayer.getCard(playerCards, humanPlayer.getCardNum, sizeof(playerCards) / sizeof(playerCards[0]));              //抽牌  获取抽牌数量humanPlayer.getCardState = false;for (int i = 0; i < humanPlayer.handCardNums; i++) {humanPlayer.handCards[i]->setState(0);       //设置贴图为普通状态}}}

属性显示优化

之前不知道怎么想的,居然用图片来绘制数字,,,忘了还有Text类,把之前的属性绘制的部分改一改

Player.h中

Font textFont;           //文字的字体
Text hpText, mpText, moveText;      //文字

构造函数中

Player::Player() {textFont.loadFromFile("./data/font/simsun.ttc");hpText.setFont(textFont);hpText.setCharacterSize(20);hpText.setPosition(620, 864);mpText.setFont(textFont);mpText.setCharacterSize(20);mpText.setPosition(920, 864);moveText.setFont(textFont);moveText.setCharacterSize(20);moveText.setPosition(1028, 870);
}

draw函数中

void Player::drawState(RenderWindow* window) {hpText.setString(to_string(hp));mpText.setString(to_string(mp));moveText.setString(to_string(movePower));window->draw(hpText);window->draw(mpText);window->draw(moveText);
}

4月18日

功能实现

  1. 部分卡牌数值
  2. 无法出牌时的提示

卡牌数值的绑定

这里先做一部分卡牌的数值,就是那些没有特殊功能的卡牌

自定义一个cardManage类来进行卡牌的管理

#pragma once
#include "Player.h"
class cardManage {public:int playerCardLength = 77, enemyCardLength = 63;Card playerCards[77], enemyCards[63];                                      //卡牌数组Texture tCard1[3][77], tEnemyCard[63];                                        //卡牌纹理数组void initCards();                                                           //初始化卡牌数据int useCard(Player*, Card*, Player*);                                      //使用卡牌void getCard(Player*, Card[], int, int, int);                             //抽牌 玩家,卡组 抽牌数 卡组数量 抽牌种类bool cardUsable(Player, Card);                                               //手牌是否可用};

为了方便管理以及提高代码可读性,我把之前在Game.cpp里加载卡牌的部分重新写在了cardManage.cpp里

数值的绑定就比较枯燥了,有规律的就批量赋值,没有规律的就只能一张一张来了

void cardManage::initCards() {//加载卡牌图片for (int i = 0; i < 77; i++) {if (i < 8) {playerCards[i].cardType = playerCards[i].equip;} else if (i < 14) {playerCards[i].cardType = playerCards[i].attack;} else if (i < 26) {playerCards[i].cardType = playerCards[i].mp;} else if (i < 45) {playerCards[i].cardType = playerCards[i].movePower;playerCards[i].cost = 1;} else if (i < 72) {playerCards[i].cardType = playerCards[i].magic;playerCards[i].cost = 1;} else if (i < 77) {playerCards[i].cardType = playerCards[i].fightBack;}}playerCards[8].damage = 6;playerCards[8].getMp = 6;playerCards[9].damage = 5;playerCards[10].damage = 5;playerCards[11].damage = 5;playerCards[12].damage = 12;playerCards[12].destory = true;playerCards[13].damage = 5;playerCards[14].destory = true;playerCards[14].getMp = 25;playerCards[15].getMp = 8;playerCards[16].getMp = 5;playerCards[16].getHp = 5;playerCards[17].getMp = 5;playerCards[17].getHp = 5;//中间就省略了for (int i = 0; i < 63; i++) {if (i < 10) {enemyCards[i].cardType = enemyCards[i].equip;} else if (i < 24) {playerCards[i].cardType = enemyCards[i].attack;} else if (i < 25) {enemyCards[i].cardType = enemyCards[i].mp;} else if (i < 60) {enemyCards[i].cardType = enemyCards[i].movePower;} else if (i < 61) {enemyCards[i].cardType = enemyCards[i].fightBack;} else if (i < 63) {enemyCards[i].cardType = enemyCards[i].magic;}}
}

抽牌也是和之前写的大同小异

void cardManage::getCard(Player* player, Card cards[], int getNum, int cardLength, int cardType) {if (player->handCardNums == player->handMaxNum) {                              //手牌已达上限return;} else if (getNum + player->handCardNums > player->handMaxNum) {               //抽牌后会达到上限getNum = player->handMaxNum - player->handCardNums;}srand((unsigned)time(NULL));                                                   //随机if (player->handCardNums <= player->handMaxNum) {for (; getNum != 0;) {int num = rand() % cardLength;for (int i = 0; i < player->handMaxNum; i++) {if (player->handCards[i] == &player->nullCard) {if (cards[num].cardState == cards[num].cardPool) {                //如果卡牌在卡池里就抽牌if (cardType < 6) {                                         //卡牌种类 大于等于6为特殊牌(暂定)if (cards[num].cardType == cardType) {                  //如果是对应种类cards[num].cardState = cards[num].handPool;           //卡牌移到手牌区player->handCardNums += 1;                            //手牌数加一player->handCards[i] = &cards[num];                  //手牌数组赋值getNum--;}}}}}}}
}

卡牌的使用

int cardManage::useCard(Player* player1, Card* card, Player* player2) {if (!cardUsable(*player1, *card)) {return card->cardType;} else {player1->handCardNums--;               //手牌数减一card->cardState = card->disCardPool;  //移到弃牌区player1->mp += card->getMp;              //获得蓝量player1->movePower += card->getMovePower; //获得行动力switch (card->cardType) {             //减去对应的消耗case card->magic:player1->mp -= card->cost;break;case card->movePower:player1->movePower -= card->cost;break;default:break;}if (player1->hp += card->getHp > player1->fullHp) {player1->hp = player1->fullHp;} else {player1->hp += card->getHp;              //获得血量}player1->hp += card->superHp;            //获得血量上限player1->fullHp += card->superHp;player2->reduceGetCardNum += card->reduceEnemyGetNum;      //减少抽牌数if (card->destory) {card->cardState = card->noPool;            //被移除}if (card->removeGame) {card->cardState = card->null;        //从游戏移去}if (card->getCardNum >= 1) {if (player1->humanPlayer) {getCard(player1, playerCards, card->getCardNum, playerCardLength, card->getCardType);} else {getCard(player1, enemyCards, card->getCardNum, enemyCardLength, card->getCardType);}}return -1;}
}

Game中调用

switch (humanPlayer.handCards[i]->checkMouse(mousePosition, event, cardOffset.x, cardOffset.y)) {     case 3:pressSd.stop();releaseSd.play();if (enemyCarpente.checkMouse(mousePosition)) {switch (cardManage.useCard(&humanPlayer, humanPlayer.handCards[i], &enemyCarpente)) {      //是否可出牌case 2:cout << "mp need";break;case 3:cout << "move need";break;case -1:attackSd.play();                                                 //播放攻击音乐humanPlayer.handCards[i] = &humanPlayer.nullCard;                  //设定使用后的手牌为nullbreak;}} else {if (humanPlayer.handCards[i]->mouseContainf(disCardBtn.getPosition(), 30, -30)) {humanPlayer.disCard(humanPlayer.handCards[i]);humanPlayer.handCards[i] = &humanPlayer.nullCard;                  //设定使用后的手牌为null} elsehumanPlayer.handCards[i]->setState(humanPlayer.handCards[i]->NORMAL);        //释放后设定为普通状态}disCardBtn.setState(disCardBtn.NORMAL);humanPlayer.cardSelect = -1;                               //没有在与卡牌进行交互humanPlayer.handCards[i]->hoverSd = true;                   //释放卡牌,音效可播放humanPlayer.handCards[i]->pressSd = true;break;
}

差不多这个样子,行动力或蓝量不足就无法出对应的牌

出牌mp不足的提示

自定义一个hintText类,来进行各种提示文本的管理

hintText.h

#pragma once
#include <string>
#include "SFML/Graphics/RenderWindow.hpp"
#include <SFML\Graphics\Text.hpp>
#include <SFML\Graphics\Color.hpp>
using namespace sf;
using namespace std;
class hintText {public:hintText();Color color;Font textFont;Text hint;Clock clock;bool isShow = false;         //是否正在绘制void setText(int);void showHint(RenderWindow*);
};

hintText.cpp

#include "hintText.h"
#include <iostream>
hintText::hintText() {textFont.loadFromFile("./data/font/simsun.ttc");
}
void hintText::setText(int type) {              //根据传入参数不同输出不同的文本hint.setFont(textFont);hint.setCharacterSize(40);switch (type) {case 0:hint.setString(L"魔力不足!");break;case 1:hint.setString(L"行动力不足!");break;}hint.setPosition(750, 400);color.r = 0;color.g = 0;color.b = 0;color.a = 255;hint.setFillColor(color);isShow = true;clock.restart();
}
void hintText::showHint(RenderWindow* window) {                     //慢慢变透明if (isShow) {if (clock.getElapsedTime().asMilliseconds() > 500) {color.a = 255 - (clock.getElapsedTime().asMilliseconds() - 500) / 10;hint.setFillColor(color);}if (clock.getElapsedTime().asMilliseconds() < 3050) {window->draw(hint);} else {isShow = false;}}
}

Game.cpp中调用

//fightInput中
switch (cardManage.useCard(&humanPlayer, humanPlayer.handCards[i], &enemyCarpente)) {       //是否可出牌case 2:hint.setText(0);break;case 3:hint.setText(1);break;case -1:attackSd.play();                                                   //播放攻击音乐humanPlayer.handCards[i] = &humanPlayer.nullCard;                  //设定使用后的手牌为nullbreak;
}
void Game::drawDialog() {///hint.showHint(&window);
}

大概效果,有一个缓慢消失的动画

4月22日

功能实现

  1. 完善抽牌
  2. 抽牌池剩余数量

完善抽牌

写一个获取抽牌区卡牌数量的功能

int cardManage::getCardPoolNum(Player player, Card cards[], int length) {int num = length;for (int i = 0; i < length; i++) {if (cards[i].cardState != cards[i].cardPool) {num--;}}if (player.humanPlayer) {          //是人类玩家playerCardPoolNum = num;return playerCardPoolNum;} else {enemyCardPoolNum = num;return enemyCardPoolNum;}
}

在抽牌区寻找符合条件的卡牌

vector<int> cardManage::searchCard(Player player, Card cards[], int length, int type, int cost) {vector<int> pos;if (getCardPoolNum(player, cards, length) == 0) {return pos;} else {switch (type) {case -1:for (int i = 0; i < length; i++) {if (cards[i].cardState == cards[i].cardPool) {pos.push_back(i);}}break;case 0:case 1:case 2:case 3:case 4:case 5:if (cost == -1) {for (int i = 0; i < length; i++) {if (cards[i].cardState == cards[i].cardPool && cards[i].cardType == type) {pos.push_back(i);}}} else {for (int i = 0; i < length; i++) {if (cards[i].cardState == cards[i].cardPool && cards[i].cardType == type && cards[i].cost == cost) {pos.push_back(i);}}}break;}}return pos;
}

再次修改之前的抽牌功能……

void cardManager::getCard(Player* player, Card cards[], int getNum, int cardLength, int cardType) {if (player->handCardNums == player->handMaxNum) {                             //手牌已达上限return;} else if (getNum + player->handCardNums > player->handMaxNum) {               //抽牌后会达到上限getNum = player->handMaxNum - player->handCardNums;}srand((unsigned)time(NULL));                                                   //随机vector<int> pos;if (player->handCardNums <= player->handMaxNum) {for (; getNum != 0;) {int num = rand() % cardLength;for (int i = 0; i < player->handMaxNum; i++) {if (getNum == 0) {return;}if (player->handCards[i] == &player->nullCard) {switch (cardType) {case -1:pos = searchCard(*player, cards, cardLength, cardType, -1);if (pos.empty()) {return;} else {int randPos = rand() % pos.size();cards[pos[randPos]].cardState = cards[pos[randPos]].handPool;         //卡牌移到手牌区player->handCardNums += 1;                                //手牌数加一player->handCards[i] = &cards[pos[randPos]];                 //手牌数组赋值getNum--;if (player->humanPlayer) {playerCardPoolNum--;} else {enemyCardPoolNum--;}break;}case 0:case 1:case 2:case 3:case 4:case 5:pos = searchCard(*player, cards, cardLength, cardType, -1);if (pos.empty()) {return;} else {int randPos = rand() % pos.size();cards[pos[randPos]].cardState = cards[pos[randPos]].handPool;           //卡牌移到手牌区player->handCardNums += 1;                            //手牌数加一player->handCards[i] = &cards[pos[randPos]];                 //手牌数组赋值getNum--;if (player->humanPlayer) {playerCardPoolNum--;} else {enemyCardPoolNum--;}break;}case 6:pos = searchCard(*player, cards, cardLength, 2, 1);                    //两张消耗为1的咒术牌if (pos.empty()) {return;} else {int randPos = rand() % pos.size();cards[pos[randPos]].cardState = cards[pos[randPos]].handPool;          //卡牌移到手牌区player->handCardNums += 1;                            //手牌数加一player->handCards[i] = &cards[pos[randPos]];                 //手牌数组赋值getNum--;if (player->humanPlayer) {playerCardPoolNum--;} else {enemyCardPoolNum--;}break;}case 7:                        //抽牌到上限break;}}}}}
}

再在玩家和敌人回合开始时判断一下抽牌池是不是空,是的话就重置

if (whosTurn == eEnemyTurn) {if (enemyCarpente.getCardState) {if (cardManage.enemyCardPoolNum == 0) {for (int i = 0; i < sizeof(cardManage.enemyCards) / sizeof(cardManage.enemyCards[0]); i++) {          //遍历if (cardManage.enemyCards[i].cardState == cardManage.enemyCards[i].disCardPool) {                 //如果是在弃牌堆就转移到抽牌堆中cardManage.enemyCards[i].cardState = cardManage.enemyCards[i].cardPool;}}}}
} else if (whosTurn == ePlayerTurn && !gameLose) {if (humanPlayer.getCardState) {if (cardManage.playerCardPoolNum == 0) {                                                                   //如果卡池抽完了for (int i = 0; i < sizeof(cardManage.playerCards) / sizeof(cardManage.playerCards[0]); i++) {           //遍历if (cardManage.playerCards[i].cardState == cardManage.playerCards[i].disCardPool) {                   //如果是在弃牌堆就转移到抽牌堆中cardManage.playerCards[i].cardState = cardManage.playerCards[i].cardPool;}}}humanPlayer.mp /= 2;                 //每回合魔法减半if (humanPlayer.movePower == 0) {        //每回合回复一点行动力humanPlayer.movePower = 1;}cardManage.getCard(&humanPlayer, cardManage.playerCards, humanPlayer.getCardNum, sizeof(cardManage.playerCards) / sizeof(cardManage.playerCards[0]), -1);}
}

抽卡池剩余卡牌绘制

void cardManager::initCards() {textFont.loadFromFile("./data/font/simsun.ttc");textCardPoolNum.setFont(textFont);textCardPoolNum.setCharacterSize(20);textCardPoolNum.setPosition(1202, 810);textCardPoolNum.setFillColor(Color::Black);
}
void cardManager::drawCardPoolNum(RenderWindow* window) {textCardPoolNum.setString(to_string(playerCardPoolNum));window->draw(textCardPoolNum);
}
void Game::drawPlayer() {cardManage.drawCardPoolNum(&window);
}

大致效果

6月7日

前言

因为5月份就回到学校了,环境跟在家里不太一样(不能通宵敲代码了),而且临近期末各个学科的大作业也ddl将至,所以几乎一个月没有碰这个游戏了,之后为了简化,又把一些卡牌都去掉了。。最后的效果也不是很满意,总之对我来说现在能做完就不错了,至少可以对战了。。

功能实现

  1. 持续状态
  2. 特殊卡牌

持续状态

绘制

我们需要将使用的持续卡牌的状态绘制出来,并让玩家属性与其交互

std::vector<int> playerStatus;             //玩家状态的数组
void drawStatus(RenderWindow* window);      //绘制卡牌状态
Texture tPlayerStatus[20];                  //状态纹理
statusBtn sPlayerStatus[20];                //状态精灵

所以准备好必要的图片

将其加载

for (int i = 0; i < 20; i++) {stringstream ss;ss << "./data/status/card" << i << ".png";tPlayerStatus[i].loadFromFile(ss.str());sPlayerStatus[i].setTexture(tPlayerStatus[i]);sPlayerStatus[i].setScale(0.3f, 0.3f);}

然后依次绘制

void Player::drawStatus(RenderWindow* window) {for (int i = 0; i < playerStatus.size(); i++) {sPlayerStatus[playerStatus[i]].setPosition(325 + i * 60.f, 790);window->draw(sPlayerStatus[playerStatus[i]]);}
}

使用持续卡牌后状态就会绘制在必要位置

数值

以下函数为了方便对特定卡牌进行测试

void Player::cheat_getCard(Card card[], int num) {if (handCardNums == handMaxNum) {return;}for (int i = 0; i < handMaxNum; i++) {if (handCards[i] == &nullCard) {if (card[num].cardState == card[num].cardPool) {                                    //如果卡牌在卡池里就抽牌card[num].cardState = card[num].handPool;                                     //卡牌移到手牌区handCardNums += 1;                                               //手牌数加一handCards[i] = &card[num];                                      //手牌数组赋值}}}
}

装备分为被动型触发和主动型触发,所以写两个不同的函数

被动触发

void Player::statusUpdate(Player* enemy, int turn) {for (int i = 0; i < playerStatus.size(); i++) {switch (playerStatus[i]) {case 0:                                                           //累计造成4伤害加1点生命值addUpDamageAble = true;if (addUpDamage / 4 >= 1) {hp += addUpDamage / 4;addUpDamage %= 4;}break;case 1:if (turn == 1 && countDamage == 0) {addUpDamage += hp / 10;enemy->hp -= hp / 10;countDamage = 1;} else if (turn == 0) {countDamage = 0;}break;default:break;}}
}

主动触发

void cardManager::playerStatus(Player* player1, Card* card, Player* player2) {for (int i = 0; i < player1->playerStatus.size(); i++) {switch (player1->playerStatus[i]) {case 2:if (card->cardType == card->magic) {player2->hp -= 1;}default:break;}}
}

总结

距离这个作业完成也有8个月左右时间了,也算是颇有意义的一次经历吧,大大提升了自己的代码水平和思考能力(虽然依旧很菜),也让自己入门了游戏开发,对游戏开发有了一些认识,之后也做了不少游戏出来,完成这篇文章时正好也是2021的新年,希望新的一年里有一个新的开始,能够做出更多,更好玩的游戏!!

使用c++SFML制作月圆之夜总集篇相关推荐

  1. asmr刷新失败无法连接上服务器_月圆之夜连接服务器失败 连接不上网络怎么办...

    月圆之夜连接服务器失败 连接不上网络怎么办.月圆之夜是一款非常好玩的网络游戏,许多游戏爱好者进入游戏想领略它的玩法,但是近日有许多小伙伴反应一直连接不上服务器,一打开就是连接网络失败这样的界面.那么这 ...

  2. 月圆之夜-体验报告(完稿时间2021/6/11)

    <月圆之夜>体验分析报告 产品概述 游戏介绍 游戏表现 美术 音效表现 系统体验分析 玩法系统 冒险玩法 拼图玩法 成就收集系统 天赋系统 战斗系统 主角 卡牌系统 剧情系统 设置系统 游 ...

  3. 月圆之夜-天赋系统策划案(完稿时间2021/6/15)

    月圆之夜天赋系统策划案 简介 设计概述 设计目的 设计思路 天赋分属 点亮条件--星星 合理的UI样式 详细设计 界面详解 功能设计 功能流程 美术需求 简介 月圆之夜天赋系统,提升玩家基于手牌战力上 ...

  4. [日记] 月圆之夜,紫金之巅

    发信人: wanily (静坐思过), 信区: Travel 标  题: 紫金之巅赏月纪实-04中秋我们一起浪漫一起疯狂 发信站: 水上明珠 (Wed Sep 29 11:15:20 2004) 斑竹 ...

  5. 《中秋书月》月圆之夜,我和德鲁克

    中秋节又到了. 今年的中秋节,期待的大闸蟹之外,另有一番文化盛宴.那就是林铁的<中秋书月>. 书法登上微信舞台,完全是今年的意外之喜,因为林铁开通微信至今不过才三个月,他也不是书法家,搞书 ...

  6. 月圆之夜,愿永无bug

    程序员在中秋节的愿望 (点击链接阅读原文)

  7. 游戏剖析与实现 之 《月圆之夜》

    最近准备开一系列自己喜欢的游戏的技术剖析以及实现,没有任何对原著的恶意,只是想思考下成功的作品怎么实现的,如果有好游戏也可以推荐我玩下,前提是有意思的耐玩的游戏 首先这个独立游戏没有其他啥界面,难点基 ...

  8. 月圆夜,又中秋,抽个奖,祝好运。

    题图:Photo by John Fowler on Unsplash 月圆夜,又中秋.祝各位中秋快乐,家圆,钱包圆,事事圆. 咱们公众号每个月至少会有一次福利,中秋福利自然少不了福利.这次联系了4家 ...

  9. 月圆“会客厅“欢度国庆节,平度举行2020中秋十一晚会

    10月1日"今天是你的生日,我的中国--"随着激昂的乐曲响起,荧光挥舞,欢声笑语,平度市奥体中心广场成了欢乐的海洋. 9月30日晚,平度市"月圆会客厅欢度国庆节" ...

最新文章

  1. Leetcode每日必刷题库第2题,如何实现两数相加?
  2. python3.X 使用pip 离线安装whl包(转载)
  3. 戴尔科技:以技术突破创新边界!
  4. NASM汇编语言与计算机系统05-以大写16进制在屏幕显示内存中的值(shr/shl/jb)
  5. 【iOS】Image图片属性之Render as Template Image
  6. C# 打开指定的目录 记住路径中 / 与 \ 的使用方法
  7. 知识分享:进销存管理系统记录有效库存让销售更有保障
  8. 从家庭主妇到格力老总,董明珠的大女主逆袭之路
  9. bzoj5369: [PKUSC2018]最大前缀和 (状压dp)
  10. 服务器弄好了怎么做网站,怎么自己做网站?自己做网站都需要什么?
  11. 浏览器事件模型捕获、冒泡
  12. 三、Solr管理控制台(二)
  13. div显示在上层_如何让div总是显示在最上层,而不致于被其他div遮挡
  14. RSA生成密钥对的过程
  15. 原生小程序 申请小程序 - 发布流程
  16. 嵌入式linux学习笔记--TCP通讯整理
  17. 从NCBI refseq 中下载特定物种的蛋白质数据
  18. elasticsearch: max virtual memory areas vm.max_map_count [65530] likely too low, increase to at leas
  19. 稳压二极管数据手册参数补充
  20. 从SGS认识晚安月亮纸尿裤,换个角度更专业

热门文章

  1. python 凯利公式_Python量化笔记-股票收益率的正态分布检验和凯利公式应用
  2. 4款口碑爆棚电脑黑科技软件,强烈建议低调使用!
  3. leaq c 汇编语言,汇编 LEA 指令
  4. 那些开源项目和编程语言背后的故事
  5. 人工智能这把火 会让.ai域名成为下一个.com吗?
  6. SAP和ISAP(网路最大流的两个增广路算法)
  7. 人力资源管理英汉词汇
  8. 51单片机 74HC154译码器制作流水灯+Proteus仿真
  9. 推荐一款C端的低代码产品
  10. 胖瘦诊断程序c语言,基于51单片机人体身高体重胖瘦检测设计 (1).doc