写在开头

文章太长了界面可能会卡顿

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

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

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

2020年4月6日

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

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

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

import cv2
import os
import numpy as np

fileList=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/

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

  1. 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) {                    //抽牌数减为0
                return;
            }
        }
    }
}

最后来看一下效果

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
                        } else
                            humanPlayer.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;            //回合开始时出牌为null
    useCardTimer.restart();            //启动计时器
}

然后是绘制卡牌

void Enemy::showCard(RenderWindow* window) {
    if (handCardNums >= 0) {
        if (useCardTimer.getElapsedTime().asMilliseconds() > 1200) {        //大于1200 ms
            for (int i = 0; i < handMaxNum; i++) {
                if (handCards[i]->cardState != handCards[i]->null) {        //如果手牌不为空
                    cardShow = *handCards[i];                                //展示该卡牌
                    useCardTimer.restart();                                    //重启计时器
                    disCard(handCards[i]);                                    //弃牌
                    handCards[i] = &nullCard;                                //手牌数组位置为null
                    return;                                                    //退出循环
                }
            }
        } else {
            if (cardShow.cardState != null) {                //展示的卡牌状态不为null
                window->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;                    //设定使用后的手牌为null
                break;
        }
    } else {
        if (humanPlayer.handCards[i]->mouseContainf(disCardBtn.getPosition(), 30, -30)) {
            humanPlayer.disCard(humanPlayer.handCards[i]);
            humanPlayer.handCards[i] = &humanPlayer.nullCard;                    //设定使用后的手牌为null
        } else
            humanPlayer.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;                    //设定使用后的手牌为null
        break;
}
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的新年,希望新的一年里有一个新的开始,能够做出更多,更好玩的游戏!!

Last modification:February 12th, 2021 at 08:50 am