1. 结束面板窗口类EndingPanel

结束面板窗口类是用于在当一个玩家打完自己手中的牌时,也就意味着游戏结束。当结束的时候,会在主窗口的正中间跳入一个结束面板窗口,上面记录了各个玩家的得分。

结束分数面板窗口类:新建、c++、c++clase、类名为EndignPanel,基类为QWidget。

1.1 EndingPanel类头文件

结束面板窗口类的头文件主要就是定义一些有关结束面板的私有变量,如背景、按钮等。同时也定义一个关于是否继续游戏的信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class EndingPanel : public QWidget
{
Q_OBJECT
public:
explicit EndingPanel(bool isLord, bool isWin, QWidget *parent = nullptr);

//获取玩家的得分,并显示
void setPlayerScore(int left, int right, int me);

signals:
void continueGame(); //添加一个自定义信号(点击继续按钮时,发出该信号)

protected:
void paintEvent(QPaintEvent *ev);

private:
QPixmap m_bk; //结束面板的背景
QLabel* m_title; //地主赢或输、农民赢或输的标签
ScorePanel *m_score; //之前创建的分数窗口
QPushButton* m_continue; //继续游戏按钮
};

1.2 EndingPanel类函数实现

这部分就是对头文件定义的函数实现,通过构造函数,直接初始化完结束面板窗口上的所有内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
EndingPanel::EndingPanel(bool isLord, bool isWin, QWidget *parent) : QWidget(parent)
{
m_bk.load(":/images/gameover.png"); //先把结束面板加载出来
setFixedSize(m_bk.size()); //设置大小为图片大小

//显示用户玩家的角色以及游戏状态
m_title = new QLabel(this); //创建一个标签
if(isLord && isWin){ //如果用户是地主,且赢了,就在标签上加载对应的图
m_title->setPixmap(QPixmap(":/images/lord_win.png"));
}else if(isLord & !isWin){
m_title->setPixmap(QPixmap(":/images/lord_fail.png"));
}else if(!isLord && isWin){
m_title->setPixmap(QPixmap(":/images/farmer_win.png"));
}else if(!isLord && !isWin){
m_title->setPixmap(QPixmap(":/images/farmer_fail.png"));
}
m_title->move(125,125); //标签显示位置

//分数面板
m_score = new ScorePanel(this);
m_score->move(75, 230);
m_score->setFixedSize(260, 160); //设置结束面板中,显示分数面板的大小
m_score->setMyFontColor(ScorePanel::Red); //设置字体颜色为红色
m_score->setMyFontSize(18); //设置字体大小为8

//继续游戏按钮
m_continue = new QPushButton(this);
m_continue->move(84, 429);
QString style = R"(
QPushButton{border-image: url(:/images/button_normal.png)}
QPushButton:hover{border-image: url(:/images/button_hover.png)}
QPushButton:pressed{border-image: url(:/images/button_pressed.png)}
)";
m_continue->setStyleSheet(style);
m_continue->setFixedSize(231,48); //按钮大小
connect(m_continue, &QPushButton::clicked, this, &EndingPanel::continueGame); //发出信号
}

void EndingPanel::setPlayerScore(int left, int right, int me)
{
m_score->setScores(left, right, me); //直接调用左上角分数面板的设置分数
}

void EndingPanel::paintEvent(QPaintEvent *ev)
{
Q_UNUSED(ev);
QPainter p(this);
p.drawPixmap(rect(), m_bk); //参1:这个图像要显示到哪个矩形区域里面
}

2. 动画效果类AnimationWindow

动画效果类主要就是负责在游戏构成中,在主窗口显示的一些动画。比如说当抢地主阶段,每个玩家的下注分数或不叫。和当有用户打出王炸、顺子、飞机等特殊牌型时,都会有触发一些动画,来提高游戏的体验感。

动画效果类创建:选择新建、c++、c++clase、类名为AnimationWindow,基类为QWidget。

2.1 AnimationWindow类头文件

动画效果类的头文件定义了一些特殊牌型的动画显示函数和下注的分数显示函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class AnimationWindow : public QWidget
{
Q_OBJECT
public:
enum Type{Sequence, Pair};
explicit AnimationWindow(QWidget *parent = nullptr);

//显示下注分数
void showBetScore(int bet);
//显示顺子和连对
void showSequence(Type type);
//显示王炸
void showJokerBomb();
//显示炸弹
void showBomb();
//显示飞机
void showPlane();

signals:

protected:
void paintEvent(QPaintEvent* ev);

private:
QPixmap m_image; //加载图片的对象
int m_index;
int m_x;
};

2.2 AnimationWindow类函数实现

这不是是对AnimationWindow类头文件中定义的函数实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
AnimationWindow::AnimationWindow(QWidget *parent) : QWidget(parent)
{
m_x = 0;
}

//显示分数窗口加载
void AnimationWindow::showBetScore(int bet) //主窗口创建AnimationWindow类对象后,会调用该函数
{
m_x = 0;
if(bet == 1){
m_image.load(":/images/score1.png");
}else if(bet == 2){
m_image.load(":/images/score2.png");
}else if(bet == 3){
m_image.load(":/images/score3.png");
}
update(); //将加载的图片显示到窗口
//当抢地主完毕之后,就隐藏该显示分数这个窗口,也就是让被实例化的AnimationWindow对象调用hide方法
QTimer::singleShot(2000, this, &AnimationWindow::hide); //定时器只触发一次(一次性信号),2秒钟后隐藏分数
}

void AnimationWindow::showSequence(AnimationWindow::Type type)
{
m_x = 0;
QString name = type == Pair ? ":/images/liandui.png" : ":/images/shunzi.png"; //通过传进来的参数加载对应的图片
m_image.load(name);
update();
QTimer::singleShot(2000,this,&AnimationWindow::hide); //2s后隐藏动画
}

void AnimationWindow::showJokerBomb()
{
m_index = 0;
m_x = 0;
QTimer* timer = new QTimer(this); //创建定时器
connect(timer, &QTimer::timeout, this, [=](){
m_index++; //每进来一次,index++
if(m_index > 8){
timer->stop(); //然后大于8了,就停止定时器
timer->deleteLater(); //销毁回收定时器
m_index = 8;
hide(); //隐藏动画
}
QString name = QString(":/images/joker_bomb_%1.png").arg(m_index); //拼接图片路径
m_image.load(name);
update();
});
timer->start(60); //指定间隔为60毫秒
}

void AnimationWindow::showBomb()
{
m_index = 0;
m_x = 0;
QTimer* timer = new QTimer(this); //创建定时器
connect(timer, &QTimer::timeout, this, [=](){
m_index++; //每进来一次,index++
if(m_index > 12){
timer->stop(); //然后大于12了,就停止定时器
timer->deleteLater(); //销毁回收定时器
m_index = 12;
hide(); //隐藏动画
}
QString name = QString(":/images/bomb_%1.png").arg(m_index); //拼接图片路径
m_image.load(name);
update();
});
timer->start(60); //指定间隔为60毫秒
}

void AnimationWindow::showPlane()
{
m_x = width(); //x轴就默认为动画窗口的宽度
m_image.load(":/images/plane_1.png");
setFixedHeight(m_image.height()); //y轴就为图片的高度
update();

int step = width() / 5; //将宽度分为5份,得到5份取区间
QTimer* timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, [=](){
static int dist = 0; //飞机移动的距离
static int timers = 0;
dist += 5;
if(dist>=step){ //如果满足,说明某一个区间已经走完了,就进入下一个区间
dist = 0;
timers++;
QString name = QString(":/images/plane_%1.png").arg(timers%5+1); //图片在1到5之间切换
m_image.load(name);
}
if(m_x <= -110){ //当飞机飞出动画窗口,就结束
timer->stop();
timer->deleteLater();
dist = timers = 0;
hide();
}
m_x -= 5; //每次往左走5像素
update();
});
timer->start(15); //每个15毫秒执行一次,画一次
}

void AnimationWindow::paintEvent(QPaintEvent *ev)
{
Q_UNUSED(ev); //处理ev参数没有使用的警告
QPainter p(this);
//因为对于飞机动画来说,m_x是移动的,而其它动画不移动,所以就把m_x初始化为0
p.drawPixmap(m_x,0,m_image.width(),m_image.height(),m_image); //加载的图片在窗口中的位置x和y;宽度和高度;QPixmap对象
}

3. 闹钟窗口类CountDown

闹钟窗口类主要是在主窗口为用户玩家创建了一个定时闹钟,轮到用户玩家出牌阶段,用户玩家只能在指定的时间内完成出牌或不出,当15秒过后,就默认玩家放弃出牌。当然,闹钟会起作用的前提是玩家必须出牌阶段,比如说当玩家抢完地主后或出牌后,没有前提玩家出牌,又该自己出时,这两种情况闹钟是不会出现的,其他情况,闹钟都会起作用。

闹钟窗口类:新建、c、c++、类名为CountDown,基类为QWidget

3.1 CountDown类头文件

闹钟窗口类CountDown的头文件就是定义了闹钟的图像对象和倒计时数字的图片对象,又定义显示闹钟函数来完成对应的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CountDown : public QWidget
{
Q_OBJECT
public:
explicit CountDown(QWidget *parent = nullptr);

void showCountDown();
void stopCountDown(); //终止倒计时的(当闹钟的秒数还没有读到0,但用户玩家已经将牌打出去了,这种情况也需要终止倒计时)

signals:
void notMuchTime(); //当闹钟的秒数减到5时,向主界面发送提醒信号
void timeout(); //当闹钟的秒数减到0时,向主界面发送信号(切换当前玩家为下一个玩家)

protected:
void paintEvent(QPaintEvent *ev);

private:
QPixmap m_clock; //加载闹钟图片对象
QPixmap m_number; //加载闹钟上的数字对象
QTimer* m_timer;
int m_count; //倒计时的总时长

};

3.2 CountDown类函数实现

这部分就是对CountDown类头文件中定义的函数实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
CountDown::CountDown(QWidget *parent) : QWidget(parent)
{
setFixedSize(70,70); //给当前闹钟窗口显示固定大小(与图片大小一样)
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, [=](){
m_count--; //每次-1
if(m_count<10 && m_count>0){ //当count大于0小于10时,显示闹钟出来
m_clock.load(":/images/clock.png");
//加载数字到闹钟上,数字的宽度是30像素,间隙是10个像素,从后从30*42像素缩放成20*30像素
m_number = QPixmap(":/images/number.png").copy(m_count*(30+10), 0, 30, 42).scaled(20,30); //x轴上是从远到近
if(m_count == 5){
emit notMuchTime(); //当秒数减为5时,发出信号(播放提示音乐)
}
}else if(m_count <= 0){
m_clock = QPixmap(); //将闹钟设置成空值(不显示了)
m_number = QPixmap(); //将数字设置成空值(不显示了)
m_timer->stop(); //停掉定时器
emit timeout(); //发出信号(切换当前玩家)
}
update();
});
}

void CountDown::showCountDown()
{
m_count = 15;
m_timer->start(1000); //启动定时器(每隔1s切换闹钟里面的数字)

}

void CountDown::stopCountDown()
{
m_timer->stop(); //停掉定时器
m_clock = QPixmap(); //将闹钟设置成空值(不显示了)
m_number = QPixmap(); //将数字设置成空值(不显示了)
update();
}

void CountDown::paintEvent(QPaintEvent *ev)
{
Q_UNUSED(ev); //处理ev参数没有使用的警告
QPainter p(this);
p.drawPixmap(rect(), m_clock); //绘制闹钟
p.drawPixmap(24,24,m_number.width(),m_number.height(),m_number); //绘制数字
}

4. 背景音乐类BGMControl

背景音乐类是负责整个游戏的一个配音,有的是循环一直播放,有的是只播放一次。比如说游戏的背景音乐就是一直播放,我们出牌时,男生和女生的声音就是只一次播放,还有一写特殊牌型的声音也是只播放一次,如飞机、炸弹等。

背景音乐类:新建、c++、c++clase、类名为BGMControl、基类为QObject。

4.1 BGMControl类头文件

背景音乐类的头文件定义了一个牌型的枚举类,它与文件中的json文件里面的配音目录顺序一致,这样方便在程序中调用。同时定义了在不同情况下,播放不同的音乐函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class BGMControl : public QObject
{
Q_OBJECT
public:
enum RoleSex{Man, Woman};
enum CardType
{
// 单张牌
Three,
Foue,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King,
Ace,
Tow,
SmallJoker,
BigJoker,
// 两张牌
Three_Double,
Foue_Double,
Five_Double,
Six_Double,
Seven_Double,
Eight_Double,
Nine_Double,
Ten_Double,
Jack_Double,
Queen_Double,
King_Double,
Ace_Double ,
Tow_Double,
// 三张牌
Three_Triple,
Foue_Triple,
Five_Triple,
Six_Triple,
Seven_Triple,
Eight_Triple,
Nine_Triple,
Ten_Triple,
Jack_Triple,
Queen_Triple,
King_Triple,
Ace_Triple,
Tow_Triple,
// 其他组合
Plane, // 飞机
SequencePair, // 连对
ThreeBindOne, // 三带一
ThreeBindPair, // 三带一对
Sequence, // 顺子
FourBindTwo, // 四带二(单张)
FourBind2Pair, // 四带两对
Bomb, // 炸弹
JokerBomb, // 王炸
Pass1, // 过
Pass2,
Pass3,
Pass4,
MoreBiger1, // 大你
MoreBiger2,
Biggest, // 压死
// 抢地主
NoOrder, // 不叫
NoRob, // 不抢
Order, // 叫地主
Rob1, // 抢地主
Rob2,
Last1, // 只剩1张牌
Last2 // 只剩2张牌
};
enum AssistMusic //辅助音乐
{
Dispatch, // 发牌
SelectCard, // 选牌
PlaneVoice, // 飞机
BombVoice, // 炸弹
Alert, // 提醒
};

explicit BGMControl(QObject *parent = nullptr);

//初始化播放列表
void initPlayList();

//1.背景音乐(播放和停止播放)
void startBGM(int volume);
void stopBGM();
//2.播放玩家抢地址音乐
void playerRobLordMusic(int point, RoleSex sex, bool isFirst);
//3.播放出牌的背景音乐
void playCardMusic(Cards cards, bool isFirst, RoleSex sex);
void playLastMusic(CardType type, RoleSex sex); //播放剩余1张或2张牌的音乐
//4.播放不出牌的背景音乐
void playPassMusic(RoleSex sex);
//5.播放辅助音乐
void playAssistMusic(AssistMusic type);
void stopAssistMusic();
//6.播放结尾音乐
void playEndingMusic(bool isWin);

signals:

private:
//0.man 1.woman 2.bgm 3.辅助音乐 4.结束音乐
QVector<QMediaPlayer*> m_players;
QVector<QMediaPlaylist* > m_lists;
};

4.2 BGMControl函数实现

这部分就是实现了BGMControl类中定义的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
BGMControl::BGMControl(QObject *parent) : QObject(parent)
{
//0.man 1.woman 2.bgm 3.辅助音乐 4.结束音乐
for(int i=0; i<5; ++i)
{
QMediaPlayer* player = new QMediaPlayer(this); //创建多媒体对象
QMediaPlaylist* list = new QMediaPlaylist(this); //创建播放列表对象
if(i<2 || i == 4) //当是0、1或4时,音乐只播放一次
{
list->setPlaybackMode(QMediaPlaylist::CurrentItemOnce);
}
else if(i == 2) //当是2时,就循环播放音乐
{
list->setPlaybackMode(QMediaPlaylist::Loop);
}
player->setPlaylist(list);
player->setVolume(100); //音量
m_players.push_back(player);
m_lists.push_back(list);
}
initPlayList();
}

//初始化播放列表
void BGMControl::initPlayList()
{
QStringList list;
list << "Man" << "Woman" << "BGM" << "Other" << "Ending";

// 读json配置文件
QFile file(":/conf/playList.json");
file.open(QFile::ReadOnly);
QByteArray json = file.readAll();
file.close();
// 解析从文件中读出的json数据
QJsonDocument doc = QJsonDocument::fromJson(json);
QJsonObject obj = doc.object();

for(int i=0; i<list.size(); ++i)
{
QString prefix = list.at(i);
QJsonArray array = obj.value(prefix).toArray();
// 初始化多媒体播放列表
for(int j=0; j<array.size(); ++j)
{
m_lists[i]->addMedia(QMediaContent(QUrl(array.at(j).toString())));
}
}
}

//游戏背景音乐
void BGMControl::startBGM(int volume)
{
//2是背景音乐,其音频对应的下标是2
m_lists[2]->setCurrentIndex(0);
m_players[2]->setVolume(volume);
m_players[2]->play(); //通过对应的多媒体对象的play播放音乐
}

void BGMControl::stopBGM()
{
m_players[2]->stop(); //停止播放背景音乐
}

// 玩家下注了没有?
// 玩家的性别?
// 什么时候播放什么样的音频文件
void BGMControl::playerRobLordMusic(int point, RoleSex sex, bool isFirst)
{
int index = sex == Man ? 0 : 1;
if(isFirst && point > 0) //如果是第一个叫地主,且下注分数大于0
{
m_lists[index]->setCurrentIndex(Order);
}
else if(point == 0) //下注分数为0,即没有叫地主
{
if(isFirst)
{
m_lists[index]->setCurrentIndex(NoOrder); //如果是第一家就播放不叫音乐
}
else
{
m_lists[index]->setCurrentIndex(NoRob); //如果不是第一家就播放不抢音乐
}
}
else if(point == 2) //下注2分
{
m_lists[index]->setCurrentIndex(Rob1);
}
else if(point == 3) //下注3分
{
m_lists[index]->setCurrentIndex(Rob2);
}
m_players[index]->play(); //通过多媒体对象播放音乐
}

//出牌的音乐
void BGMControl::playCardMusic(Cards cards, bool isFirst, RoleSex sex)
{
// 得到播放列表
int index = sex == Man ? 0 : 1;
QMediaPlaylist* list = m_lists[index]; //通过男生或女生,得到相应的播放列表对象

Card::CardPoint pt = Card::CardPoint::Card_Begin;
// 取出牌型 然后进行判断
PlayHand hand(cards);
PlayHand::HandType type = hand.getHandType();
if(type == PlayHand::Hand_Single || type == PlayHand::Hand_Pair || type == PlayHand::Hand_Triple)
{
pt = cards.takeRandomCard().point(); //如果是单牌、双牌或三牌,取出其点数(取出随机一张牌即可)
}
int number = 0;
switch(type)
{
// 单牌
case PlayHand::Hand_Single:
number = pt - 1; //因为pt的枚举类是从begin开始的,需要减1才能和json文件里面的枚举类一一对应
break;
// 对牌
case PlayHand::Hand_Pair:
number = pt - 1 + 15; //对牌在单牌后面,所以需要跳过单牌个数15
break;
// 三张点数相同的牌
case PlayHand::Hand_Triple:
number = pt - 1 + 15 + 13; //跳过对牌个数13
break;
// 三带一
case PlayHand::Hand_Triple_Single:
number = ThreeBindOne;
break;
// 三带二
case PlayHand::Hand_Triple_Pair:
number = ThreeBindPair;
break;
// 飞机
case PlayHand::Hand_Plane:
// 飞机带两个单
case PlayHand::Hand_Plane_Two_Single:
// 飞机带两个对儿
case PlayHand::Hand_Plane_Two_Pair:
number = Plane;
break;
// 连对
case PlayHand::Hand_Seq_Pair:
number = SequencePair;
break;
// 顺子
case PlayHand::Hand_Seq_Single:
number = Sequence;
break;
// 炸弹
case PlayHand::Hand_Bomb:
number = Bomb;
break;
// 王炸
case PlayHand::Hand_Bomb_Jokers:
number = JokerBomb;
break;
// 炸弹带一对儿
case PlayHand::Hand_Bomb_Pair:
// 炸弹带两单
case PlayHand::Hand_Bomb_Two_Single:
// 王炸带一对儿
case PlayHand::Hand_Bomb_Jokers_Pair:
// 王炸带两单
case PlayHand::Hand_Bomb_Jokers_Two_Single:
number = FourBindTwo;

default:
break;
}

if(!isFirst && (number >= Plane && number <= FourBindTwo))
{
list->setCurrentIndex(MoreBiger1 + QRandomGenerator::global()->bounded(2));
}
else
{
list->setCurrentIndex(number);
}
// 播放音乐
m_players[index]->play();
if(number == Bomb || number == JokerBomb)
{
playAssistMusic(BombVoice);
}
if(number == Plane)
{
playAssistMusic(PlaneVoice);
}
}

void BGMControl::playLastMusic(CardType type, RoleSex sex)
{
// 1. 玩家的性别
int index = sex == Man ? 0 : 1;
// 2. 找到播放列表
QMediaPlaylist* list = m_lists[index];
if(m_players[index]->state() == QMediaPlayer::StoppedState) //如果当前对应的多媒体播放是空闲状态
{
list->setCurrentIndex(type);
m_players[index]->play();
}
else
{
QTimer::singleShot(1500, this, [=](){
list->setCurrentIndex(type);
m_players[index]->play();
});
}
}

void BGMControl::playPassMusic(RoleSex sex)
{
// 1. 玩家的性别
int index = sex == Man ? 0 : 1;
// 2. 找到播放列表
QMediaPlaylist* list = m_lists[index];
// 3. 找到要播放的音乐
int random = QRandomGenerator::global()->bounded(4); //随机生成一个数字,0到3
list->setCurrentIndex(Pass1 + random); //随机播放不要音乐
// 4. 播放音乐
m_players[index]->play();
}

void BGMControl::playAssistMusic(AssistMusic type)
{
QMediaPlaylist::PlaybackMode mode; //定义一个播放模式
if(type == Dispatch) //如果是发牌状态,就循环播放音乐
{
// 循环播放
mode = QMediaPlaylist::CurrentItemInLoop;
}
else //其它情况,只播放一次
{
// 单曲播放一次
mode = QMediaPlaylist::CurrentItemOnce;
}
// 2. 找到播放列表
QMediaPlaylist* list = m_lists[3];
// 3. 找到要播放的音乐
list->setCurrentIndex(type);
list->setPlaybackMode(mode);
// 4. 播放音乐
m_players[3]->play();
}

void BGMControl::stopAssistMusic() //停止辅助音乐播放
{
m_players[3]->stop();
}

//最后输或赢播放音乐
void BGMControl::playEndingMusic(bool isWin)
{
if(isWin)
{
m_lists[4]->setCurrentIndex(0);
}
else
{
m_lists[4]->setCurrentIndex(1);
}
m_players[4]->play();
}

5. 游戏主窗口类GamePanel

游戏主窗口类是游戏的展示界面,玩家玩游戏的整个过程都是在这个窗口中显示的。

5.1 GamePanel类头文件

游戏窗口类的头文件需要定义很多初始化的函数,同时也需要接收很多发送过来的信号,然后通过相应的函数(槽函数)来实现对应的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class GamePanel : public QMainWindow
{
Q_OBJECT

public:
GamePanel(QWidget *parent = nullptr);
~GamePanel();

enum AnimationType{ShunZi, LianDui, Plane, JokerBomb, Bomb, Bet}; //效果动画类要显示的一些动画
//初始化游戏控制类信息
void gameControlInit(); //该函数也负责信号槽的一些连接
//更新分数面板的分数
void updatePlayerScore();
//切割并存储图片
void initCardMap();
//裁剪图片
void cropImage(QPixmap& pix, int x, int y, Card& c); //参数:裁剪图片对象,x位置,y位置,卡牌对象(需要它的花色和点数)
//初始化游戏按钮组
void initButtonsGroup();
//初始化玩家在窗口中的上下文环境(每个玩家的位置信息)
void initPlayerContext();
//初始化游戏场景
void initGameScene();
//处理游戏的状态
void gameStatusPrecess(GameControl::GameStatus status); //参数枚举类型游戏状态,有发牌、叫地主和出牌
//发牌
void startDispatchCard();
//移动扑克牌
void cardMoveStep(Player* player, int curPos);
//处理分发得到的扑克牌(发牌时卡牌区域更新)
void disposeCard(Player* player, const Cards& cards); //参数:处理的牌属于哪个玩家,要处理的牌(一张或多张)
//更新扑克牌在窗口中的显示(把玩家得到的牌更新到该显示的位置上,和打出的牌更新到出牌区域)
void updatePlayerCards(Player* player); //更新的是哪个玩家的扑克牌窗口
//加载玩家的头像
QPixmap loadRoleImage(Player::Sex sex, Player::Direction direct, Player::Role role);

//定时器的处理动作
void onDispatchCard();
//处理玩家状态的变化(槽函数)
void onPlayerStatusChanged(Player* player, GameControl::PlayerStatus status);
//处理玩家抢地主情况,再在主窗口显示(槽函数)
void onGrabLordBet(Player* player, int bet, bool flag); //参数:具体哪个玩家,下注分数,是否是第一个叫地主的玩家
//处理玩家的出牌显示
void onDisposePlayHand(Player* player, Cards& cards);
//处理用户玩家鼠标选牌(槽函数)
void onCardSelected(Qt::MouseButton button);
//处理用户玩家出牌(槽函数)
void onUserPlayHand();
//处理用户放弃出牌(槽函数)
void onUserPass();

//显示特效动画
void showAnimation(AnimationType type, int bet=0); //因为室友特效动画都是该函数显示,所以直接传入动画枚举类,当是抢地主分数的时候,会用到参2
//隐藏玩家打出的牌
void hidePlayerDropCards(Player* player);
// 显示玩家的最终得分(结束面板设置)
void showEndingScorePanel();
//初始化闹钟倒计时
void initCountDown();

protected:
// 重写父类的虚函数---事件处理函数
void paintEvent(QPaintEvent* ev);
void mouseMoveEvent(QMouseEvent* ev);


private:
enum CardAlign{Horizontal, Vertical}; //对齐方式
struct PlayerContext{ //每个玩家有一个这样的结构体,是存放玩家对应的位置信息
//1.玩家扑克牌显示的区域
QRect cardRect;
//2.出牌的区域
QRect playHandRect;
//3.扑克牌的对齐方式(水平 or 垂直)
CardAlign align;
//4.扑克牌显示正面还是背面
bool isFrontSide;
//5.游戏过程中的提示信息,比如:不出
QLabel* info;
//6.玩家的头像
QLabel* roleImg;
//7.玩家刚打出的牌
Cards lastCards;
};
Ui::GamePanel *ui;
QPixmap m_bkImage; //背景图片的加载对象
GameControl* m_gameCtl; //游戏控制类对象
QVector<Player*> m_playerList; //存放三个玩家对象的实例
QMap<Card, CardPanel*>m_cardMap; //保存卡牌数据和卡牌窗口对象的容器
QSize m_cardSize; //卡牌大小(宽和高)
QPixmap m_cardBackImg; //卡牌背景图加载对象
QMap<Player*, PlayerContext> m_contextMap; //将玩家与其对应的位置结构体存到map

CardPanel* m_baseCard; //刚开始,发牌区的扑克牌
CardPanel* m_moveCard; //移动中的扑克牌
QVector<CardPanel*>m_last3Card; //最后三张底牌

QPoint m_baseCardPos; //起始时,牌的初始位置(中心偏上一点)
GameControl::GameStatus m_gameStatus; //存放游戏状态
QTimer* m_timer; //定时器

AnimationWindow* m_animation; //动画效果类对象

CardPanel* m_curSelCard; //鼠标选择的卡牌窗口对象(只限自己手牌的,因为点击其它机器人玩家的牌,在onCardSelected()函数中已经被排除了)
QSet<CardPanel*> m_selectCards; //保存用户鼠标选择的卡牌窗口对象
//下面两个在updatePlayerCards()里面初始化的
QRect m_cardsRect; //存储用户玩家剩余的牌显示区域
QHash<CardPanel*,QRect> m_userCards; //记录用户玩家手中的牌,以及每张牌在窗口中的位置

CountDown* m_countDown; //闹钟类对象指针

BGMControl* m_bgm; //BGM音乐类对象
};

5.2 GamePanel类函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
GamePanel::GamePanel(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::GamePanel)
{
ui->setupUi(this);

// 1. 背景图
int num = QRandomGenerator::global()->bounded(10); //生成0到9随机数
QString path = QString(":/images/background-%1.png").arg(num+1); //拼接背景图路径
m_bkImage.load(path); //随机加载一张背景图(刚开始时),通过事件处理器函数painEvent就能将背景图画到窗口上了,当窗口初始化好后,painEvent会自动调用

//2.窗口的标题和大小(固定)
this->setWindowTitle("欢乐斗地主");
this->setFixedSize(1000, 650);

// 3. 实例化游戏控制类对象
gameControlInit();

// 4. 玩家得分(更新)
updatePlayerScore();

// 5. 切割游戏图片
initCardMap();

// 6. 初始化游戏中的按钮组
initButtonsGroup();

// 7. 初始化玩家在窗口中的上下文环境
initPlayerContext();

// 8. 扑克牌场景初始化
initGameScene();

// 9. 倒计时窗口初始化
initCountDown();

// 定时器实例化
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &GamePanel::onDispatchCard); //每触发一次定时器,就会调用槽函数,发牌

m_animation = new AnimationWindow(this); //创建一个动画效果类,指定了父对象,它就显示在父对象窗口上面,而且没有边框
m_bgm = new BGMControl(this);
}

GamePanel::~GamePanel()
{
delete ui;
}

void GamePanel::gameControlInit() //实例化游戏控制类对象
{
m_gameCtl = new GameControl(this); //指定父对象,窗口析构的时候,就会先自动析构m_gameCtl了
m_gameCtl->playerInit(); //三个玩家对象被创建出来了
//得到三个玩家的实例对象
Robot* leftRobot = m_gameCtl->getLeftRobot();
Robot* rightRobot = m_gameCtl->getRightRobot();
UserPlayer* user = m_gameCtl->getUserPlayer();
m_playerList << leftRobot << rightRobot << user; //存储顺序:左侧机器人,右侧机器人,用户玩家

//GameControl类发出的一个信号(玩家状态变化),主窗口接收并处理相应的槽函数
connect(m_gameCtl, &GameControl::playerStatusChanged, this, &GamePanel::onPlayerStatusChanged);
//GameControl类发出的一个信号(抢地主的情况),主窗口接收并处理相应的槽函数
connect(m_gameCtl, &GameControl::notifyGrabLordBet, this, &GamePanel::onGrabLordBet);
//GameControl类发出的一个信号(游戏状态变化),主窗口接收并处理相应的槽函数
connect(m_gameCtl, &GameControl::gameStatusChanged, this, &GamePanel::gameStatusPrecess);
//GameControl类发出的一个信号(玩家出牌了),主窗口接收并处理相应的槽函数(处理一些动画效果,更新玩家的牌,并将出的牌显示在出牌区域)
connect(m_gameCtl, &GameControl::notifyPlayHand, this, &GamePanel::onDisposePlayHand);

//不管是哪个玩家得到牌后都会发出一个信号,主窗口接收该信号
connect(leftRobot, &Player::notifyPickCards, this, &GamePanel::disposeCard);
connect(rightRobot, &Player::notifyPickCards, this, &GamePanel::disposeCard);
connect(user, &Player::notifyPickCards, this, &GamePanel::disposeCard);
}


void GamePanel::updatePlayerScore()
{
ui->scorePanel->setScores( //setScores函数负责将传入的得分显示在窗口上
m_playerList[0]->getScore(),
m_playerList[1]->getScore(),
m_playerList[2]->getScore());
}

void GamePanel::initCardMap()
{
//1.加载大图
QPixmap pixmap(":/images/card.png");
//2.计算每张图片的大小
m_cardSize.setWidth(pixmap.width()/13);
m_cardSize.setHeight(pixmap.height()/5);
//3.背面图加载(裁剪),需要提供背景图左上角的点,copy(x轴,y轴,要裁剪的宽度,要裁剪的高度)
m_cardBackImg = pixmap.copy(2*m_cardSize.width(), 4*m_cardSize.height(), m_cardSize.width(), m_cardSize.height());
//正常花色
for(int i=0, suit=Card::Suit_Begin+1; suit<Card::Suit_End; suit++,i++){
for(int j=0,pt=Card::Card_Begin+1; pt<Card::Card_SJ; pt++,j++){
Card card((Card::CardPoint)pt, (Card::CardSuit)suit);
//裁剪图片(参数:大图对象,裁剪卡牌的x轴,裁剪卡牌的y轴,对应的单张卡牌对象(记录的是对应的花色和点数))
cropImage(pixmap, j*m_cardSize.width(), i*m_cardSize.height(), card);
}
}
//小王
Card c;
c.setPoint(Card::Card_SJ);
c.setSuit(Card::Suit_Begin);
cropImage(pixmap, 0, 4*m_cardSize.height(), c);
//大王
c.setPoint(Card::Card_BJ);
c.setSuit(Card::Suit_Begin);
cropImage(pixmap, m_cardSize.width(), 4*m_cardSize.height(), c);
}


void GamePanel::cropImage(QPixmap &pix, int x, int y, Card& c) //裁剪图片
{
QPixmap sub = pix.copy(x, y, m_cardSize.width(), m_cardSize.height()); //得到一张扑克牌正面
CardPanel* panel = new CardPanel(this); //创建一张卡牌窗口对象
panel->setImage(sub, m_cardBackImg); //参数:正面图片对象,背面图片对象 ----->图片显示的已经是对应的花色和点数了
panel->setCard(c); //设置花色和点数 ----->这相当于是记录该图片的花色和点数
panel->hide(); //隐藏
m_cardMap.insert(c,panel); //裁剪成功的卡牌存入容器,方便后期管理和处理
//接收CardPanel类发出的鼠标选择信号--->每张牌的窗口对象只要满足条件,都可以发出信号
connect(panel, &CardPanel::cardSelected, this, &GamePanel::onCardSelected);
}

void GamePanel::initButtonsGroup()
{
ui->btnGroup->initButtons(); //所有的按钮初始化完成
ui->btnGroup->selectPanel(ButtonGroup::Start); //开始时,显示的是开始游戏页面
//处理各个按钮发出的信号
//初始页面的开始按钮,当发出startGame信号,就说明开始游戏的按钮被按下了
connect(ui->btnGroup, &ButtonGroup::startGame, this, [=](){
//界面的初始化
ui->btnGroup->selectPanel(ButtonGroup::Empty); //选择空页面,这样窗口的所有按钮都被隐藏了
m_gameCtl->clearPlayerScore(); //将所有玩家的得分清0(在构造函数时调用过了,可写可不写)
updatePlayerScore(); //更新所有玩家的得分(在构造函数时调用过了,可写可不写)
//修改游戏状态 ---> 发牌
gameStatusPrecess(GameControl::DispatchCard); //基于这个参数,里面是通过switcha函数来执行不同游戏状态下的函数
//播放背景音乐
m_bgm->startBGM(80); //指定音量为80
});
connect(ui->btnGroup, &ButtonGroup::playHand, this, &GamePanel::onUserPlayHand); //当点击了出牌按钮,会发出playHand信号
connect(ui->btnGroup, &ButtonGroup::pass, this, &GamePanel::onUserPass);
connect(ui->btnGroup, &ButtonGroup::betPoint, this, [=](int bet){ //叫地主几分按钮
m_gameCtl->getUserPlayer()->grabLordBet(bet);
ui->btnGroup->selectPanel(ButtonGroup::Empty); //按完下注分数按钮后,就执行选择空页面,这样窗口的所有按钮都被隐藏了
});
}


void GamePanel::initPlayerContext() //为每个玩家设置对应的位置信息
{
//1.设置玩家扑克牌的区域
const QRect cardsRect[]={
//x,y,width,heught
QRect(90,130,100,height()-200), //左侧机器人
QRect(rect().right()-190, 130, 100, height()-200), //右侧机器人
QRect(250, rect().bottom()-120, width()-500,100) //当前玩家
};
//2.玩家出牌的区域
const QRect playHandRect[]={
QRect(260,150,100,100), //左侧机器人
QRect(rect().right()-360, 150, 100, 100), //右侧机器人
QRect(150, rect().bottom()-290, width()-300,105) //当前玩家
};
//3.玩家头像显示的位置
const QPoint roleImgPos[]={
QPoint(cardsRect[0].left()-80,cardsRect[0].height()/2+20), //左侧机器人
QPoint(cardsRect[1].right()+10,cardsRect[1].height()/2+20), //右侧机器人
QPoint(cardsRect[2].right()-10,cardsRect[2].top()-10) //当前玩家
};

//循环
int index = m_playerList.indexOf(m_gameCtl->getUserPlayer()); //得到m_playerList存放用户玩家的下标索引(是2,但是不想把代码写死,所以通过这行代码获取)
for(int i=0; i<m_playerList.size();i++){ //变量三个玩家
PlayerContext context; //为每个玩家创建各自的PlayerContext位置结构体
context.align = i==index ? Horizontal:Vertical; //对齐方式:机器人玩家和用户玩家是不一样的
context.isFrontSide = i==index ? true:false; //扑克牌展示正面还是背面,需要判定,i=index,说明是用户玩家
context.cardRect = cardsRect[i]; //卡牌区域(因为m_playerList和cardsRect的顺序是一样的,所以直接赋值即可)
context.playHandRect = playHandRect[i]; //出牌区域
//提示信息(每个玩家的提示信息位置)
context.info = new QLabel(this); //创建一个标签
context.info->resize(160, 98); //指定标签大小
context.info->hide(); //初始是隐藏的
//将提示信息显示到出牌区域的中心位置
QRect rect = playHandRect[i]; //每个玩家的出牌区域
QPoint pt(rect.left()+(rect.width()-context.info->width())/2, rect.top()+(rect.height()-context.info->height())/2); //获取出牌区域的中心位置,需要(出牌区域框架-提示信息框架)/2
context.info->move(pt); //将提示信息移动到pt位置
//玩家头像
context.roleImg = new QLabel(this); //创建一个放头像的标签
context.roleImg->resize(84, 120); //固定大小
context.roleImg->hide(); //隐藏
context.roleImg->move(roleImgPos[i]); //将头像标签移动到指定位置
m_contextMap.insert(m_playerList.at(i), context); //key存玩家对象,value存对应的位置结构体context
}
}

void GamePanel::initGameScene()
{
//下面三部分,每一部分都对应一张或多张扑克牌,每张扑克牌都是一个卡牌窗口,所以需要创建卡牌窗口对象
//1.发牌区的扑克牌
m_baseCard = new CardPanel(this);
m_baseCard->setImage(m_cardBackImg, m_cardBackImg); //两面都显示背面即可
// 2. 发牌过程中移动的扑克牌
m_moveCard = new CardPanel(this);
m_moveCard->setImage(m_cardBackImg, m_cardBackImg); //两面都显示背面即可
// 3. 最后的三张底牌(用于窗口的显示)
for(int i=0; i<3; ++i)
{
CardPanel* panel = new CardPanel(this);
panel->setImage(m_cardBackImg, m_cardBackImg); //两面都显示背面即可
m_last3Card.push_back(panel);
panel->hide(); //初始先隐藏
}
//发牌区和移动过程扑克牌的位置(同一个)
m_baseCardPos = QPoint((width() - m_cardSize.width()) / 2, height() / 2 - 100); //中心位置偏上一点
m_baseCard->move(m_baseCardPos); //设置发牌区的扑克牌位置
m_moveCard->move(m_baseCardPos); //设置移动过程中扑克牌的位置
//底牌位置(三种牌的x轴不一样,y轴一样)
int base = (width() - 3 * m_cardSize.width() - 2 * 10) / 2; //width()是当前窗口的总宽度,三张牌有2个空隙(2*10)
for(int i=0; i<3; ++i)
{
m_last3Card[i]->move(base + (m_cardSize.width() + 10) * i, 20); //分别设置三张底牌的位置
}
}

void GamePanel::gameStatusPrecess(GameControl::GameStatus status) //处理游戏的状态
{
//记录游戏状态
m_gameStatus = status;
//处理游戏状态
switch(status){
case GameControl::DispatchCard: //开始发牌状态
startDispatchCard(); //调用发牌函数
break;
case GameControl::CallingLord: //叫地主状态
{
//取出底牌数据
CardList last3Card = m_gameCtl->getSurplusCards().toCardList(); //将存放三张底牌的set容器转为了QVector容器
//给底牌窗口设置图片
for(int i=0; i<last3Card.size(); i++){ //遍历三张底牌
QPixmap front = m_cardMap[last3Card.at(i)]->getImage(); //获取每张牌它的一个图像
m_last3Card[i]->setImage(front,m_cardBackImg); //设置该卡牌的正面图片和反面图片
m_last3Card[i]->hide(); //先是隐藏的
}
//开始叫地主
m_gameCtl->startLordCard(); //gamecontrol的一个函数,里面会发出一个信号,主窗口再接收处理槽函数onPlayerStatusChanged
break;
}
case GameControl::PlayingHand: //出牌状态(抢完地主)
//隐藏发牌区的底牌和移动的牌
m_baseCard->hide();
m_moveCard->hide();
//显示留给地主的三张底牌(顶部)
for(int i=0; i<m_last3Card.size(); i++){
m_last3Card.at(i)->show();
}
for(int i=0; i<m_playerList.size(); i++){
PlayerContext &context = m_contextMap[m_playerList.at(i)]; //获取每个玩家的位置结构体
context.info->hide(); //隐藏各个玩家抢地主过程中的提示信息
//显示各个玩家的头像
Player* player = m_playerList.at(i);
QPixmap pixmap = loadRoleImage(player->getSex(),player->getDirection(), player->getRole()); //得到对应玩家的头像
context.roleImg->setPixmap(pixmap); //设置头像
context.roleImg->show(); //显示出来
}
break;
default:
break;
}
}

void GamePanel::startDispatchCard() //发牌
{
//重置每张卡牌的属性(不是所有属性) QMap<Card, CardPanel*>m_cardMap;
for(auto it=m_cardMap.begin(); it!=m_cardMap.end(); it++){
it.value()->setSeclected(false); //设置非选中状态
it.value()->setFrontSide(true); //显示正面图片
it.value()->hide(); //窗口隐藏
}
//隐藏三张底牌
for(int i=0; i<m_last3Card.size();i++){
m_last3Card.at(i)->hide();
}
//重置玩家的窗口上下文信息(位置)
int index = m_playerList.indexOf(m_gameCtl->getUserPlayer()); //传入用户玩家对象,得到它在m_playerList的下标
for(int i=0; i<m_playerList.size(); i++){ //对玩家容器进行遍历
m_contextMap[m_playerList.at(i)].lastCards.clear(); //玩家上一次打出的牌进行清空
m_contextMap[m_playerList.at(i)].info->hide(); //隐藏游戏过程中的提示信息
m_contextMap[m_playerList.at(i)].roleImg->hide(); //玩家的头像隐藏
m_contextMap[m_playerList.at(i)].isFrontSide = i==index ? true : false; //显示正面还是背面(用户玩家显示正面,机器人玩家显示背面)
}
//重置所有玩家的卡牌数据
m_gameCtl->resetCardData(); //该函数里面是做了:洗牌、清空三个玩家的手牌、将出牌玩家对象指向空和出的牌清空
//显示底牌
m_baseCard->show(); //正中间的扑克牌,刚开是背面时(显示出来)
//隐藏按钮面板
ui->btnGroup->selectPanel(ButtonGroup::Empty); //传入的是空窗口
//启动定时器
m_timer->start(10); //每隔10毫秒,定时器就触发一次
//播放背景音乐
m_bgm->playAssistMusic(BGMControl::Dispatch); //播放发牌的音乐
}

void GamePanel::cardMoveStep(Player *player, int curPos) //参数:具体是哪个玩家,移动步长的次数
{
//得到每个玩家的扑克牌展示区域
QRect cardRect = m_contextMap[player].cardRect;
//每个玩家的单位步长
const int unit[] = {(m_baseCardPos.x() - cardRect.right())/100, (cardRect.left() - m_baseCardPos.x())/100, (cardRect.top() - m_baseCardPos.y())/100 }; //左侧玩家:(发牌区域x坐标-左侧玩家区域右变x轴)/长度;右侧玩家
//每次窗口移动的时候,每个玩家对应的移动牌的实时坐标位置
const QPoint pos[]={ QPoint(m_baseCardPos.x() - curPos * unit[0], m_baseCardPos.y()), QPoint(m_baseCardPos.x() + curPos * unit[1], m_baseCardPos.y()), QPoint(m_baseCardPos.x(), m_baseCardPos.y() + curPos * unit[2])}; //左侧机器人x轴坐标越来越小;右侧机器人x轴坐标越来越大
//移动扑克牌窗口
int index = m_playerList.indexOf(player); //得到对应玩家的下标
m_moveCard->move(pos[index]); //卡牌移动到对应玩家 移动路径上 的某一点
//临界状态处理
if(curPos == 0){
m_moveCard->show(); //说明该牌才刚开始移动,想要展示
}
if(curPos == 100){
m_moveCard->hide(); //说明该牌已经到了对应玩家的卡牌区域,就需要隐藏了
}
}

//得到一张或多张卡牌时,会发出一个信号告诉主窗口,该函数就是接收该信号的槽函数
void GamePanel::disposeCard(Player *player, const Cards &cards) //卡牌区域更新
{
Cards& myCard = const_cast<Cards&>(cards);
CardList list = myCard.toCardList();
//CardList list = cards.toCardList(); //CardList是QVector<Card>的一个别名
for(int i=0; i<list.size(); i++){
CardPanel* panel = m_cardMap[list.at(i)]; //通过Card取出对应的窗口对象
panel->setOwner(player); //设置该张卡牌窗口的所有者
}
//更新扑克牌在窗口中的显示
updatePlayerCards(player);
}

void GamePanel::updatePlayerCards(Player *player) //把玩家得到的牌更新到该显示的位置上
{
Cards cards = player->getCards(); //取出该玩家得到的牌(手牌)
CardList list = cards.toCardList(); //将得到的牌存放到QVector,默认是降序排列

m_cardsRect = QRect(); //先将m_cardsRect初始化
m_userCards.clear(); //每次进来要清空,因为手牌位置发生变化了
//取出展示扑克牌的区域
int cardSpace = 20; //卡牌间相隔的像素(也是每张牌能显示出来的宽度)
QRect cardsRect = m_contextMap[player].cardRect; //取出该玩家的手牌区域坐标
for(int i=0; i<list.size(); i++){ //遍历每一张手牌
CardPanel* panel = m_cardMap[list.at(i)]; //每一张牌对应一张卡牌窗口
panel->show(); //显示窗口
panel->raise(); //让当前的卡牌窗口升起来(就是让当前卡牌窗口是所有之前出现的卡牌子窗口中最上层那个)
panel->setFrontSide(m_contextMap[player].isFrontSide); //玩家的卡牌(手牌)显示正面还是反面
//水平 or 垂直显示 ----->下面是为了让卡牌显示在卡牌区域的正中间(卡牌区域要大一点)
if(m_contextMap[player].align == Horizontal){ //如果是水平(用户玩家)
//(总宽度-牌占用的宽度)/2:卡牌区域左边距离+(卡牌区域总宽度 - (卡牌总数量-1)*间隔 - 最后一张牌的完整宽度)/2
int leftX = cardsRect.left()+(cardsRect.width()-(list.size()-1)*cardSpace - panel->width())/2;
int topY = cardsRect.top() + (cardsRect.height() - m_cardSize.height())/2; //卡牌区域上面距离+(卡牌区域高度-卡牌高度)/2
if(panel->isSelected()){ //如果这张扑克牌是被选中了,有各向上的弹跳效果
topY -= 10; //y轴像素-10
}
panel->move(leftX + cardSpace*i, topY); //移动卡牌窗口坐标位置
m_cardsRect = QRect(leftX, topY, cardSpace*i+m_cardSize.width(),m_cardSize.height());
int curWidth = 0;
if(list.size()-1 == i){ //如果是最后一张牌了,则该牌的宽度是一整张
curWidth = m_cardSize.width();
}else{
curWidth = cardSpace; //如果不是最后一张牌,则宽度就是间隙
}
QRect cardRect(leftX + cardSpace*i, topY, curWidth, m_cardSize.height());
m_userCards.insert(panel, cardRect); //将每张牌对象和在窗口的位置

}else{ //如果是垂直(机器人玩家)
//因为不是在窗口的最左测,所以还是需要加上cardsRect.left()
int leftX = cardsRect.left() + (cardsRect.width() - m_cardSize.width())/2; //卡牌区域左边距离+(卡牌区域宽度-卡牌宽度)/2
int topY = cardsRect.top() + (cardsRect.height() - (list.size()-1)*cardSpace - panel->height())/2; //m_cardSize和panel都可以表示牌的高度
panel->move(leftX, topY+i*cardSpace); //移动卡牌窗口坐标位置
}
}
//显示玩家打出的牌
//得到当前玩家的出牌区域以及本轮打出的牌
QRect playCardRect = m_contextMap[player].playHandRect;
Cards lastCards = m_contextMap[player].lastCards; //玩家本轮打出的牌
if(!lastCards.isEmpty()){ //如果不为空(本轮出牌了)
int playSpacing = 24; //出牌区域中牌与牌的间隙
CardList lastCardList = lastCards.toCardList(); //将出的牌存到QVector容器
CardList::ConstIterator itplayed = lastCardList.constBegin();
for(int i=0; itplayed!=lastCardList.constEnd(); itplayed++,i++){ //通过常量迭代器遍历本轮出的牌
CardPanel* panel = m_cardMap[*itplayed]; //得到对应的卡牌窗口对象
panel->setFrontSide(true); //显示正面
panel->raise(); //提升牌(让当前窗口能显示在同以级别窗口的上方)--->当有多张考牌时,为了达到堆叠的效果,2叠1,3叠2,4叠3
//将打出的牌移动到出牌区域
if(m_contextMap[player].align == Horizontal){ //水平
int leftBase = playCardRect.left()+(playCardRect.width()-(lastCardList.size()-1)*playSpacing-panel->width())/2;
int top = playCardRect.top() + (playCardRect.height()-panel->width())/2;
panel->move(leftBase+i*playSpacing, top); //牌与牌之间有空隙
}else{
int left = playCardRect.left()+(playCardRect.width()-panel->width())/2; //出牌区域左像素+卡牌x在出牌区域x的中点像素
int top = playCardRect.top(); //y轴就是出牌区域的y轴
panel->move(left, top+i*playSpacing); //牌与牌之间有空隙
}
panel->show(); //不确定是隐藏还是显示的,预防是隐藏的,所以就将其显示出来
}
}
}

QPixmap GamePanel::loadRoleImage(Player::Sex sex, Player::Direction direct, Player::Role role) //参2是图像的显示方位
{
//找图片
QVector<QString>lordMan;
QVector<QString>lordWoman;
QVector<QString>farmerMan;
QVector<QString>farmerWoman;
lordMan << ":/images/lord_man_1.png" << ":/images/lord_man_2.png";
lordWoman << ":/images/lord_woman_1.png" << ":/images/lord_woman_2.png";
farmerMan << ":/images/farmer_man_1.png" << ":/images/farmer_man_2.png";
farmerWoman << ":/images/farmer_woman_1.png" << ":/images/farmer_woman_2.png";

//通过QImage加载图片,因为其提供镜像操作(将朝向左的图片变为朝向右的图片)
QImage image;
int random = QRandomGenerator::global()->bounded(2); //随机用一张图片
if(sex==Player::Man && role==Player::Lord){ //如果是男性,且是地主
image.load(lordMan.at(random));
}else if(sex==Player::Man && role==Player::Farmer){ //如果是男性,且是农民
image.load(farmerMan.at(random));
}else if(sex==Player::Woman && role==Player::Lord){ //如果是女性,且是地主
image.load(lordWoman.at(random));
}else if(sex==Player::Woman && role==Player::Farmer){ //如果是女性,且是农民
image.load(farmerWoman.at(random));
}
QPixmap pixmap;
if(direct == Player::Left){ //如果图片是显示在左侧的,就不需要做镜像
pixmap = QPixmap::fromImage(image); //因为返回值是QPixmap,所以还得进行转换
}else{
pixmap = QPixmap::fromImage(image.mirrored(true,false)); //参1:是否做水平镜像;参2:是否做垂直镜像
}
return pixmap;
}

void GamePanel::onDispatchCard() //每触发一次定时器就会执行该函数
{
//记录扑克牌的位置(各玩家的牌移动距离都分100次步长完成)
static int curMovePos = 0; //主要是记录移动了多少个步长(第一次进该函数执行一次后,以后进该函数,都不执行该句)
//获取当前玩家
Player* curPlayer = m_gameCtl->getCurrentPlayer();
if(curMovePos >= 100){ //满足条件的话,就说明当前玩家得到了牌,轮到下一个玩家得牌了
//给当前玩家发一张牌
Card card = m_gameCtl->takeOneCard(); //随机获取一张牌
curPlayer->storeDispatchCard(card); //将该牌存入到当前玩家的手牌中
//Cards cs(card); //带参构造函数
//disposeCard(curPlayer,cs); //卡牌区域更新,该函数在上面执行storeDispatchCard()时,里面就发出了信号,并执行disposeCard

//切换当前玩家
m_gameCtl->setCurrentPlayer(curPlayer->getNextPlayer()); //将下一个玩家改为当前玩家,轮到发牌给他了
curMovePos = 0; //重新将步长次数置为0
//发牌动画
cardMoveStep(curPlayer, curMovePos);
//判断牌是否发完了
if(m_gameCtl->getSurplusCards().cardCount()==3){
//终止定时器
m_timer->stop();
//切换游戏状态
gameStatusPrecess(GameControl::CallingLord); //切换为叫地主状态
//终止发牌音乐的播放(牌发完了)
m_bgm->stopAssistMusic();
return;
}
}
//移动扑克牌
cardMoveStep(curPlayer, curMovePos); //参数:当前玩家,移动步长的次数
curMovePos += 15;
}

//处理玩家状态的变化(槽函数)--->gamecontrol类发出的信号(玩家状态变化)
void GamePanel::onPlayerStatusChanged(Player *player, GameControl::PlayerStatus status)
{
switch(status){
case GameControl::ThinkingForCallLord: //考虑叫地主
if(player == m_gameCtl->getUserPlayer()){ //只有当前玩家是用户玩家时,才执行下面(机器人玩家没有该按钮组窗口)
ui->btnGroup->selectPanel(ButtonGroup::CallLord, m_gameCtl->getPlayerMaxBet()); //切换按钮组窗口为抢地主的窗口(修改目前下注最高分)
}
break;
case GameControl::ThinkingForPlayHand: //考虑出牌
//玩家出牌前,隐藏上一轮打出的牌
hidePlayerDropCards(player);
if(player == m_gameCtl->getUserPlayer()){ //先判断玩家是否是用户玩家
//取出出牌玩家的对象
Player* pendPlayer = m_gameCtl->getPendPlayer(); //取出上一次的出牌玩家,如果为空,说明出牌刚开始,
if(pendPlayer == m_gameCtl->getUserPlayer() || pendPlayer == nullptr){ //如果用户玩家是上一次的出牌玩家(说明上一次出的牌,机器人玩家不要)
ui->btnGroup->selectPanel(ButtonGroup::PlayCard); //显示相应的按钮组窗口(这是用户玩家自由出牌)
}else{ //不是上一次的出牌玩家(轮到自己了,可以出牌,也可以不出牌)
ui->btnGroup->selectPanel(ButtonGroup::PassOrPlay);
}
}else{ //如果玩家是机器人玩家,则就不用显示空即可
ui->btnGroup->selectPanel(ButtonGroup::Empty);
}
break;
case GameControl::Winning:
m_bgm->stopBGM(); //结束播放背景音乐
//如果玩家赢了,就要显示出其它玩家的牌(由背面变为正面)
m_contextMap[m_gameCtl->getLeftRobot()].isFrontSide = true; //先将遍历置为true
m_contextMap[m_gameCtl->getRightRobot()].isFrontSide = true;
updatePlayerCards(m_gameCtl->getLeftRobot()); //updatePlayerCards会将剩余的牌会显示正面
updatePlayerCards(m_gameCtl->getRightRobot());
//更新玩家的得分
updatePlayerScore();
m_gameCtl->setCurrentPlayer(player); //将本局赢的玩家又设为当前玩家(下一局由它先决定要不要地主)
showEndingScorePanel(); //显示游戏结束面板
break;
default:
break;
}
}

void GamePanel::onGrabLordBet(Player *player, int bet, bool flag) //处理抢地主情况的槽函数(显示在主窗口)
{
//显示抢地主的信息提示
PlayerContext context = m_contextMap[player];
if(bet == 0){ //分数为0,说明不叫地主
context.info->setPixmap(QPixmap(":/images/buqinag.png"));
}else{
if(flag){ //如果是第一次叫地主
context.info->setPixmap(QPixmap(":/images/jiaodizhu.png"));
}else{
context.info->setPixmap(QPixmap(":/images/qiangdizhu.png"));
}
//显示叫地主的分数(如果不叫地主,下注分数是0,就没有必要显示,所以这句代码写在else语句里)
showAnimation(Bet,bet); //调用动画效果类。参1是动画效果枚举类的分数枚举,参2是具体的分数
}
context.info->show(); //显示出提示信息
//播放分数的背景音乐
m_bgm->playerRobLordMusic(bet, (BGMControl::RoleSex)player->getSex(),flag); //性别需要强制转换
}

void GamePanel::onDisposePlayHand(Player *player, Cards &cards)
{
//存储玩家当轮打出的牌
auto it = m_contextMap.find(player); //获取出牌玩家的一些位置信息
it->lastCards = cards;
//2.根据牌型播放游戏特效
PlayHand hand(cards);
PlayHand::HandType type = hand.getHandType(); //获得牌的类型
if(type == PlayHand::Hand_Plane || type==PlayHand::Hand_Plane_Two_Pair || type== PlayHand::Hand_Plane_Two_Single){
showAnimation(Plane);
}else if(type == PlayHand::Hand_Seq_Pair){
showAnimation(LianDui);
}else if(type == PlayHand::Hand_Seq_Single){
showAnimation(ShunZi);
}else if(type == PlayHand::Hand_Bomb){
showAnimation(Bomb);
}else if(type == PlayHand::Hand_Bomb_Jokers){
showAnimation(JokerBomb);
}
// 如果玩家打出的是空牌(不出牌),显示提示信息
if(cards.isEmpty()){
it->info->setPixmap(QPixmap(":/images/pass.png"));
it->info->show(); //把info对应的窗口显示出来(之前被隐藏掉了)
m_bgm->playPassMusic((BGMControl::RoleSex)player->getRole()); //播放不要音乐
}else{ //如果不为空,就需要判断是不是第一个(自由)出牌的玩家
if(m_gameCtl->getPendPlayer()==player || m_gameCtl->getPendPlayer()==nullptr){ //如果是自由出牌
m_bgm->playCardMusic(cards, true, (BGMControl::RoleSex)player->getSex()); //播放音乐,是自由出牌,参2为true
}else{
m_bgm->playCardMusic(cards, false, (BGMControl::RoleSex)player->getSex());
}
}
//3.更新玩家剩余的牌
updatePlayerCards(player);
//4. 播放提示音乐
//判断玩家剩余的牌的数量
if(player->getCards().cardCount() == 2){ //剩余2张
m_bgm->playLastMusic(BGMControl::Last2, (BGMControl::RoleSex)player->getSex());
}else if(player->getCards().cardCount() == 1){ //剩余1张
m_bgm->playLastMusic(BGMControl::Last1, (BGMControl::RoleSex)player->getSex());
}
}

//该函数是一个槽函数,是接收到control发出的键盘选择信号后,执行的槽函数
void GamePanel::onCardSelected(Qt::MouseButton button)
{
//1. 判断是不是出牌状态
if(m_gameStatus==GameControl::DispatchCard || m_gameStatus==GameControl::CallingLord){ //如果当前是发牌或叫地主状态,直接返回
return;
}
//2. 判断发出信号的牌的所有者是不是当前用户玩家(有时候我们的鼠标点击了机器人玩家的牌。这样对应的牌窗口也会发出信号,所以需要进行判断)
CardPanel* panel = (CardPanel*)sender(); //sender()返回的是QObject类型的指针(),转换后,得到发出信号的卡牌窗口对象
if(panel->getOwner() != m_gameCtl->getUserPlayer()){ //判断发出信号的卡牌窗口的所有者,如果不是用户对象,就直接返回
return;
}
//3. 保存当前被选中的牌的窗口对象
m_curSelCard = panel; //记录在当前手牌中选择的卡牌对象
//4. 判断参数的鼠标键是左键还是右键
if(button == Qt::LeftButton){ //点击的是左键,就设置对应的卡牌窗口加入到准备出牌的容器中
//设置扑克牌的选中状态
panel->setSeclected(!panel->isSelected()); //设置选择状态(如果之前没有选中,经过该函数就变为选中,之前选中了,经过该函数就是不选中)
//更新扑克牌在窗口中的显示
updatePlayerCards(panel->getOwner());
//保存或删除扑克牌窗口对象
QSet<CardPanel*>::const_iterator it = m_selectCards.find(panel);
if(it == m_selectCards.constEnd()){
m_selectCards.insert(panel); //没有找到,将该牌添加到容器里面
}else{
m_selectCards.erase(it);
}
m_bgm->playAssistMusic(BGMControl::SelectCard); //播放出牌音乐
}else if(button == Qt::RightButton){ //点击的是右键,就打出牌(如果不满足出牌规则,就不响应)
//调用出牌按钮对应的槽函数
onUserPlayHand(); //调用处理用户玩家出牌函数
}
}

//处理用户玩家出牌
void GamePanel::onUserPlayHand()
{
//判断游戏状态
if(m_gameStatus != GameControl::PlayingHand){ //当前游戏状态不是出牌状态,就直接退出
return;
}
//判断玩家是不是用户玩家
if(m_gameCtl->getCurrentPlayer() != m_gameCtl->getUserPlayer()){ //当前玩家如果不是用户玩家,也直接返回
return;
}
//判断要出的牌是否为空
if(m_selectCards.isEmpty()){ //用户玩家要出的牌容器如果为空,也直接退出
return;
}
//得到要打出的牌的牌型(m_selectCards容器不为空)
Cards cs;
for(auto it=m_selectCards.begin(); it!=m_selectCards.end(); it++){
Card card = (*it)->getCard();
cs.add(card); //将用户要打出的牌都放到Cards容器
}
PlayHand hand(cs);
PlayHand::HandType type = hand.getHandType(); //获取要打出牌的类型
if(type==PlayHand::Hand_Unknown){ //如果打出的牌类型是不规则(没有定义)的,也就直接退出
return;
}
//判断当前玩家的牌能不能压住上一家的牌
if(m_gameCtl->getPendPlayer() != m_gameCtl->getUserPlayer()){ //先判断上一次的出牌玩家是不是用户玩家,如果不是,就不能随意出牌(要大过对方)
Cards cards = m_gameCtl->getPendCards(); //获取上一次的出牌玩家打出的牌
if(!hand.canBeat(PlayHand(cards))){ //判断用户玩家准备打出的牌能否击败对方的牌,不能就直接退出
return;
}
}
m_countDown->stopCountDown(); //出完牌了,停止倒计时
//通过玩家对象出牌(调用出牌函数)
m_gameCtl->getUserPlayer()->playHand(cs); //playHand()函数里面会从手牌中移除要打出的牌,并发出信号,通知主窗口接收
//清空容器
m_selectCards.clear(); //清空用户玩家准备出牌的这个容器
}

//处理用户玩家的放弃出牌
void GamePanel::onUserPass()
{
m_countDown->stopCountDown(); //用户玩家放弃出牌,终止倒计时
//判断是不是用户玩家
Player* curPlayer = m_gameCtl->getCurrentPlayer(); //获取当前玩家
Player* userPlayer = m_gameCtl->getUserPlayer(); //获取用户玩家
if(curPlayer != userPlayer){ //如果当前玩家不是用户玩家,就直接退出
return;
}
//判断当前用户玩家是不是上一次出牌的玩家(可以不处理)
Player* pendPlayer = m_gameCtl->getPendPlayer();
if(pendPlayer==userPlayer || pendPlayer==nullptr){ //如果上一次出牌玩家是自己(其它玩家要不起出的牌)或者第一次出牌,这两种清空都直接退出(因为必须要出牌)
return;
}
//打出一个空的Cards是对象
Cards empty;
userPlayer->playHand(empty);
//清空用户选择的牌(玩家可能选择了一些牌,但是没有打出去)
for(auto it=m_selectCards.begin(); it!=m_selectCards.end(); it++){
(*it)->setSeclected(false); //将选中的牌都设置为非选中
}
m_selectCards.clear(); //清空准备出牌容器
//更新玩家待出牌区域的牌
updatePlayerCards(userPlayer);
}

//特效动画函数
void GamePanel::showAnimation(GamePanel::AnimationType type, int bet)
{
switch(type){
case AnimationType::LianDui:
case AnimationType::ShunZi:
m_animation->setFixedSize(250, 150); //设置窗口的固定大小(为图片的大小)
//将子窗口移动到主窗口的某一个位置,就要用到move方法
m_animation->move((width()-m_animation->width())/2, 200); //x和y位置
m_animation->showSequence((AnimationWindow::Type)type); //调用加载动画的函数
break;
case AnimationType::Plane:
m_animation->setFixedSize(800, 75); //设置窗口的固定大小(为图片的大小)
//将子窗口移动到主窗口的某一个位置,就要用到move方法
m_animation->move((width()-m_animation->width())/2, 200); //x和y位置
m_animation->showPlane(); //调用加载动画的函数
break;
case AnimationType::Bomb:
m_animation->setFixedSize(180, 200); //设置窗口的固定大小(为图片的大小)
//将子窗口移动到主窗口的某一个位置,就要用到move方法
m_animation->move((width()-m_animation->width())/2, (height()-m_animation->height())/2-70); //x和y位置
m_animation->showBomb(); //调用加载动画的函数
break;
case AnimationType::JokerBomb:
m_animation->setFixedSize(250, 200); //设置窗口的固定大小(为图片的大小)
//将子窗口移动到主窗口的某一个位置,就要用到move方法
m_animation->move((width()-m_animation->width())/2, (height()-m_animation->height())/2-70); //x和y位置
m_animation->showJokerBomb(); //调用加载动画的函数
break;
case AnimationType::Bet: //如果是抢地主分数
m_animation->setFixedSize(160, 98); //设置窗口的固定大小(为图片的大小)
//将子窗口移动到主窗口的某一个位置,就要用到move方法
m_animation->move((width()-m_animation->width())/2, (height()-m_animation->height())/2-140); //x和y都先取中间位置,y还需要往上移
m_animation->showBetScore(bet); //调用加载分数的函数
break;
}
//这个主要是考虑到其它动画,因为显示分数是在2s后,会将AnimationType窗口隐藏起来,这样其它动画窗口会看不见,所以在这里调用一下显示
m_animation->show(); //让隐藏的窗口再显示出来
}

//隐藏玩家打出的牌
void GamePanel::hidePlayerDropCards(Player *player)
{
auto it = m_contextMap.find(player); //根据当前玩家得到对应的一些信息的位置(如果没有找到传进去的玩家,就返回迭代器end)
if(it != m_contextMap.end()){ //如果it不等于end,说明找到了
if(it->lastCards.isEmpty()){ //如果为空,说明上次没有出牌
it->info->hide(); //先隐藏对应的提示信息
}else{
//Cards ---> Card 注:CardList = QVector<Card>
CardList list = it->lastCards.toCardList(); //将最后打出的牌全部存入到QVector容器
for(auto last=list.begin(); last!=list.end(); last++){ //遍历每一张牌
m_cardMap[*last]->hide(); //每一张牌都有对应的CardPanel,存到m_cardMap容器的,进行隐藏
}
}
it->lastCards.clear(); //清空玩家最后一次打出的牌容器(不然会堆叠)
}
}

//显示玩家的最终得分(结束面板设置)
void GamePanel::showEndingScorePanel()
{
bool islord = m_gameCtl->getUserPlayer()->getRole() == Player::Lord?true:false; //玩家是否是地主
bool isWin = m_gameCtl->getUserPlayer()->isWin(); //玩家是否获胜
EndingPanel* panel = new EndingPanel(islord, isWin, this); //创建一个结束面板窗口,参3的this表示在主窗口中显示
panel->show(); //显示
panel->move((width()-panel->width())/2, -panel->height()); //结束面板刚开始显示在主窗口外(看不见)
panel->setPlayerScore(m_gameCtl->getLeftRobot()->getScore(),
m_gameCtl->getRightRobot()->getScore(),
m_gameCtl->getUserPlayer()->getScore());
//根据玩家的输赢来播放对应的音乐
if(isWin){
m_bgm->playEndingMusic(true);
}else{
m_bgm->playEndingMusic(false);
}
//设置结束面板的动画效果
QPropertyAnimation *animation = new QPropertyAnimation(panel, "geometry", this); //参1:给哪个对象指定动画效果;参3:父对象
//动画持续时间
animation->setDuration(1500); //1.5s
//设置窗口的起始位置和终止位置
animation->setStartValue(QRect(panel->x(), panel->y(), panel->width(), panel->height()));
animation->setEndValue(QRect((width()-panel->width())/2, (height()-panel->height())/2, panel->width(), panel->height()));
//设置窗口的运动曲线
animation->setEasingCurve(QEasingCurve(QEasingCurve::OutBounce));
//播放动画效果
animation->start();
//处理窗口信号(当点击继续游戏按钮,就会发出continueGame信号,然后这里也会接收该信号)
connect(panel, &EndingPanel::continueGame, this, [=](){
panel->close(); //关闭结束界面窗口,但没有析构(但由于指定了父对象,所以的父对象没有结束,也就不会析构panel)
panel->deleteLater(); //手动析构panel结束界面窗口
animation->deleteLater(); //手动析构动画对象
ui->btnGroup->selectPanel(ButtonGroup::Empty); //按钮组隐藏,显示空按钮的按钮组
gameStatusPrecess(GameControl::DispatchCard); //将游戏状态设置位发牌状态
m_bgm->startBGM(80); //播放背景音乐(开始)
});
}

void GamePanel::initCountDown()
{
m_countDown = new CountDown(this); //创建一个闹钟类对象,父对象为主窗口
m_countDown->move((width()-m_countDown->width())/2, (height()-m_countDown->height())/2+30); //显示位置
connect(m_countDown, &CountDown::notMuchTime, this, [=](){ //当闹钟秒数减为5s时,会发出信号
//播放提示音乐
m_bgm->playAssistMusic(BGMControl::Alert);
});
connect(m_countDown, &CountDown::timeout, this, &GamePanel::onUserPass); //当秒数减为0s时,会发出信号,执行的槽函数当放弃出牌情况处理
UserPlayer* userPlayer = m_gameCtl->getUserPlayer(); //得到用户对象
connect(userPlayer, &UserPlayer::startCountDown, this, [=](){ //当用户玩家准备出牌时,会发出信号startCountDown
//要在主屏幕上显示闹钟,必须是用户玩家可以放弃出牌的情况,比如说上一次出牌玩家不是自己且不是第一次刚开始出牌
if(m_gameCtl->getPendPlayer()!=userPlayer && m_gameCtl->getPendPlayer()!=nullptr){
m_countDown->showCountDown();
}
});
}

void GamePanel::paintEvent(QPaintEvent *ev)
{
Q_UNUSED(ev);
QPainter p(this);
p.drawPixmap(rect(),m_bkImage); //参数:窗口矩形区域、
}

void GamePanel::mouseMoveEvent(QMouseEvent *ev) //鼠标移动过程中框选多张扑克牌
{
Q_UNUSED(ev);
//ev->buttons()里面有多种鼠标按住的移动方式(如左键、右键等)
if(ev->buttons() & Qt::LeftButton){ //判断一下是否有鼠标左键(大于0说明左键参与了行动)
QPoint pt = ev->pos(); //得到鼠标在窗口中的位置
if(!m_cardsRect.contains(pt)){ //如果pt不在出牌区域
m_curSelCard = nullptr; //将鼠标选择的卡牌窗口置为nullptr
}else{ //如果鼠标按住的位置在卡牌区域,就要找到是哪些卡牌
QList<CardPanel*> list = m_userCards.keys();
for(int i=0; i<list.size(); i++){
CardPanel* panel = list.at(i);
if(m_userCards[panel].contains(pt) && m_curSelCard!=panel){ //如果找到对应卡牌,且如果一直在一张卡牌上面,也只模拟点击一次
//点击这张扑克牌(模拟点击效果)
panel->clicked(); //该函数会发出一个信号,接收者是GamePanel
m_curSelCard = panel;
}
}
}
}
}

6. 游戏的启动过程

在这个斗地主的小游戏中,虽然创建了许多类对象,但基本上会在主窗口类中做初始化,并实现各个类的功能,将每个类的功能结合在一起,就实现了单机版的斗地主小游戏。

首先是在主窗口类GamePanel中,设置了主窗口的左上角标题和窗口大小,又往主窗口中加载了背景图,背景图有9张,每次启动游戏随机取1张。

initButtonsGroup()函数中先初始化5个页面的按钮组,选择初始界面的按钮组为开始界面,即只有一个开始按钮。然后通过信号槽机制,当点击开始按钮后,就执行匿名函数(发牌操作)。这个匿名函数会切换按钮组窗口,切换成空按钮的窗口,将所有玩家的得分清0,然后又马上更新每个玩家的得分。通过函数gameStatusPrecess()将游戏状态改为发牌状态。在gameStatusPrecess()函数里,因为传进来的参数是发牌状态的参数,通过switch执行对应的状态分支,即开始发牌函数startDispatchCard()。在该开始发牌函数中,它会设置各个玩家和扑克牌在窗口中显示的一些属性,如玩家的头像显示、玩家的手牌是否可见以及正中间的卡牌。同时,在这个函数中,也启动了一个定时器,该定时器每隔10毫秒会发送一次,该定时器是定义在主窗口类GamePanel的构造函数中的,它是每隔固定时间会执行发牌函数onDispatchCard(),这个函数中,是慢慢移动卡牌位置,然后利用定时器实现一个动态的发牌效果,当还剩下3张牌时,停止定时器,通过函数gameStatusPrecess()切换游戏状态为叫地主状态。在该状态下,通过switch执行了对应的分支。在该分支下,先是将3张底牌隐藏起来,然后调用游戏控制类的开始叫地主startLordCard()函数。这个函数里面执行了从父类继承下来的虚函数prepareCallLord(),机器人玩家和用户玩家作为子类,它们实现的操作不一样。首先是用户玩家,用户玩家在这个虚函数里面没有执行任何操作,然后又向主窗口发出了信号playerStatusChanged,主窗口类的gameControlInit()函数里面通过connect来处理该信号,令onPlayerStatusChanged()槽函数执行相应操作。在该函数里,会先判断是否是用户玩家,如果是用户玩家,就切换按钮组窗口,切换叫地主的一些按钮出来。

用户玩家的叫地主按钮显示出来后,又回到了initButtonsGroup()函数,在该函数,通过信号槽机制connect设置,当点击对应下注按钮时,会携带对应的分数执行匿名函数,在这个匿名函数中,因为用户玩家已经下注了,所以就切换按钮组窗口,切换为空按钮组。同时它会执行Player类的grabLordBet()函数,这个函数会发出信号notifyGrabLordBet,在发出该信号后,在主窗口的构造函数中,游戏控制类初始化函数gameControlInit()里面执行的playerInit()函数,在该函数里面会通过信号槽机制connect来接收信号,并执行槽函数onGrabBet(),在这个函数里面,它会根据用户玩家的下注分数,来判断是否还需要继续执行抢地主的操作,大概有两种情况:

1.用户玩家按的按钮是不抢地主、下注1分或下注2分,这些情况都说明还需要继续执行抢地主状态,就发送一个信号notifyGrabLordBet,主窗口函数会在构造函数的gameControlInit()函数里面通过信号槽来接收处理,执行槽函数onGrabLordBet(),这个函数里面就是显示提示信息和播放对应的背景音乐。然后回到onGrabBet()函数,它会切换玩家,将下一个玩家更新为当前玩家,并向主窗口发出信号playerStatusChanged,主窗口接收该信号后执行的槽函数中,通过switch执行对应的抢地主分支,而抢地主分支是只有用户玩家时,才会切换出叫地主的按钮组,不是用户玩家,就什么都不执行,退出即可。然后回到onGrabBet()函数,通过当前玩家(机器人玩家)对象调用准备叫地主函数prepareCallLord()。在机器人玩家类中,重写的这个函数里面会创建一个叫地主的子线程,该子线程会先睡眠2s(模拟思考效果),然后执行考虑叫地主函数thinkCallLord(),在该函数里,会分析对应机器人玩家的手牌情况,看是否满足叫地主的条件,通过最后计算出来的权重来断定下注多数分。然后发送信号notifyGrabLordBet,游戏控制类接受信号并执行槽函数onGrabBet(),在这个函数如果不满足某些条件,就会继续切换下一个玩家叫地主,依次下去。当满足一定条件时,就会确定地主玩家,执行成为地主函数becomeLord()

2.用户玩家下注3分,直接成为地主,执行函数becomeLord()

becomeLord()函数中,先是设置各个玩家的身份,将当前地主身份的玩家设置为当前玩家(先出牌),又将三张底牌添加到地主玩家的手牌中。然后这定义了一个定时器,每隔1s就发送信号gameStatusChangedplayerStatusChanged。其中由主窗口接收信号gameStatusChanged,执行槽函数gameStatusPrecess(),这个槽函数里面就是通过switch语句执行对应的分支,该分支下主要就是将底牌、中间的发牌隐藏、提示信息等,和显示玩家带身份的头像;主窗口也接收playerStatusChanged信号,并执行槽函数onPlayerStatusChanged(),通过执行对应的分支,该分支主要就是判断当前是否是用户玩家,如果是用户玩家,就切换按钮组窗口为出牌情况,不是的话就切换为空按钮主窗口。在处理完两个信号的槽函数后,在定时器中又调用了对应玩家的出牌函数preparePlayHand(),这个出牌函数是用户玩家和机器人玩家执行的程序不一样,用户玩家就执行发出一条信号startCountDown,用于出牌倒计时即可;机器人玩家会创建对应的出牌子线程,在子线程中会执行考虑出牌函数thinkPlayHand(),接下来就是小项目中设定的一些策略了,机器人玩家在什么情况下出什么样的牌。

7. 游戏效果

当点击启动程序时,会先显示一个加载的界面,如下图:

当加载页面完成后,就会出现游戏场景的页面了,在这个页面中右上角显示的是个玩家的分数,因为才启动程序,所以都是0分。场景的正中间是发卡牌的扑克牌,下面是开始按钮。

当点击开始游戏按钮后,就进入了发牌阶段,只有用户玩家的手牌是显示正面的,其它两个机器人玩家的手牌是显示背面的。

当发牌结束后,就游戏就进入了叫地主状态,当用户玩家直接点击3分按钮后,就可以直接成为地主,结束叫地主状态。

而当用户玩家点击除3分按钮的其它按钮,就会轮到下一个(机器人)玩家考虑是否叫地主。

当3个玩家都下注后,最后由下注分数最大的玩家当地主,其它两个玩家作为农民。并且三张底牌显示在正上方。

叫地主状态结束后,用鼠标选择卡牌,点击出牌即可。

当出的牌是一些特殊牌型,如炸弹、飞机、顺子等,还会有特效出现。

当某个玩家出完牌后,表示胜利,然后正中间出现结束面板,显示当前三个玩家的得分,同时右上角也显示当前三个玩家的得分。

当点击继续游戏按钮时,可以马上又进入下一局游戏,主窗口的右上角依然保存三个玩家的得分。