“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情”
贴上源码 👉🏻 github.com/lulusir/mal…
在线地址 👉🏻 lulusir.github.io/malegema/
期待宝子们的star⭐️
这游戏那么🔥,具体需求就不说啦
首先来看一下界面
界面元素 0. 卡牌,就是一张张的麻将
- 牌堆,用来放置剩余的牌
- 卡槽,用来存放已选择的牌
代码分析
辅助类
Card
- 卡片类定义属性和方法,具体看下方代码
- setRect用于设置卡片的xyz
- intersect用来判断两张卡片是否重叠
- delete设置过渡的删除状态以及删除状态
export class Card {
static CardWidth = 40;
static CardHeight = 56;
static _id = 0
constructor(public type: ECardType) {
this.img = ImagesMap[this.type];
}
id = Card._id++
left = 0;
top = 0;
z = 0;
width = Card.CardWidth;
height = Card.CardHeight;
img = '';
blocked = false;
deleted = false;
deleting = false;
setRect(
opt: Partial<{
left: number;
top: number;
width: number;
height: number;
}>,
) {
this.left = opt.left ?? this.left;
this.top = opt.top ?? this.top;
this.width = opt.width ?? this.width;
this.height = opt.height ?? this.height;
}
intersect(b: Card) {
const a = this;
return (
Math.abs(a.left + a.width / 2 - b.left - b.width / 2) <
(a.width + b.width) / 2 &&
Math.abs(a.top + a.height / 2 - b.top - b.height / 2) <
(a.height + b.height) / 2
);
}
/**
* 设置删除动画
*/
delete() {
this.deleting = true;
return new Promise((resolve) => {
setTimeout(() => {
this.deleting = false;
this.deleted = true;
resolve(null)
}, 300);
})
}
}
Layer
层级控制器
- 用来设置卡片的层级和位置
- 我们用网格来放置每一层的卡片,比如下面这样的网格,1表示有卡片,0表示没有
1 | 0 |
---|---|
1 | 0 |
- 通过改变层级的偏移量,以及z属性,来实现不同层级卡片的堆叠效果
- 通过init方法,把传进来的卡组设置好对应的left,top,z
export class Layer {
/**
* 从底部为第一层,每一层zindex为 10, 20, 30
* @param level
*/
static levelZ = (level = 1) => {
return level * 10;
};
left = 0;
top = 0;
z = 0;
data: (Card | 0)[][] = [[]]; // 0 表示没有卡片
init(data: (Card | 0)[][], left: number, top: number, z: number) {
this.left = left;
this.top = top;
this.z = Layer.levelZ(z);
this.data = data;
this.setCardsPosition();
}
/**
* 对当前层级的卡片设置位置
*/
setCardsPosition() {
if (this.data) {
this.data.forEach((col, j) => {
col.forEach((v, i) => {
if (v) {
v.setRect({
left: i * Card.CardWidth + this.left,
top: j * Card.CardHeight + this.top,
});
v.z = this.z;
}
});
});
}
}
}
Slot
卡槽,用来放置已选择的卡片,并且判断消除卡片
- add方法,为卡槽添加卡片,判断是否结束游戏
- refresh方法,更新卡槽中卡片数据
- remove方法,消除卡片
export class CardSlot {
left = 0;
top = 400;
data: Card[] = [];
add(card: Card) {
if (this.data.length < 7) {
card.setRect({
left: this.left + Card.CardWidth * this.data.length,
top: this.top,
});
this.data.push(card);
return true;
} else {
// 通知游戏结束
return false;
}
}
refresh() {
this.data.forEach((v, i) => {
v.setRect({
left: this.left + Card.CardWidth * i,
top: this.top,
});
});
}
remove(card: Card) {
let count = 0;
this.data.forEach((v) => {
if (v.type === card.type) {
count += 1;
}
});
let success = count === 3;
if (success) {
const p = Promise.all(this.data.filter( v => v.type === card.type).map(v => v.delete()))
this.data = this.data.filter((v) => v.type !== card.type);
this.refresh();
return p
}
return Promise.resolve()
}
}
Presenter
- 先定义界面要展示的元素
interface IViewState {
// 每一张卡牌的属性
data: {
id: number
left: number;
top: number;
z: number; // 层级
width: number;
height: number;
img: string;
blocked: boolean; // 是否被遮挡
deleted: boolean; // 是否被消除
deleting: boolean; // 删除中,用于删除动画
}[];
// 插槽的位置
slot: {
top: number
left: number
}
}
const defaultState: () => IViewState = () => {
return {
data: [],
slot: {
top: 0,
left: 0
}
};
};
@injectable()
export class GamePresenter extends Presenter<IViewState> {
constructor(public slot: CardSlot, public layer: Layer) {
super();
this.state = defaultState();
}
allCard: Card[] = [];
setSlot() {
this.setState(s => {
s.slot.top = this.slot.top
s.slot.left = this.slot.left
})
}
initCards() {
let nums = 16 + 12 + 8;
const types = Array(nums / 3)
.fill(0)
.map((_, i) => {
return i % 7;
});
const cards: Card[] = [];
for (let i = 0; i < nums; i++) {
const element = new Card(types[i % 12]);
cards.push(element);
}
const data = this.shuffle(cards);
function getCard() {
const c = data.pop();
return c;
}
// 分配卡片,初始化层级
this.layer.init(
[
[getCard(), getCard(), getCard(), getCard(), getCard()],
[getCard(), 0, 0, 0, getCard()],
[getCard(), 0, 0, 0, getCard()],
[getCard(), 0, 0, 0, getCard()],
[getCard(), getCard(), getCard(), getCard(), getCard()],
],
0,
0,
1,
);
this.layer.init(
[
[getCard(), getCard(), getCard(), getCard()],
[getCard(), 0, 0, getCard()],
[getCard(), 0, 0, getCard()],
[getCard(), getCard(), getCard(), getCard()],
],
20,
30,
2,
);
this.layer.init(
[
[getCard(), getCard(), getCard()],
[getCard(), 0, getCard()],
[getCard(), getCard(), getCard()],
],
40,
60,
3,
);
this.allCard = cards;
}
shuffle(input: any[]) {
const arr = [...input];
let length = arr.length,
temp,
random;
while (0 !== length) {
random = Math.floor(Math.random() * length);
length -= 1;
temp = arr[length];
arr[length] = arr[random];
arr[random] = temp;
}
return arr;
}
init() {
this.setSlot()
this.initCards()
this.updateBlock();
this.updateView()
}
click(id: number) {
const c = this.allCard.find(v => v.id === id)
if (c) {
if (!c.blocked) {
const canContinue = this.slot.add(c);
if (!canContinue) {
alert('游戏结束咧')
} else {
this.slot.remove(c).then(() => {
this.updateBlock()
this.updateView()
});
this.updateBlock()
this.updateView()
}
}
}
}
/**
* 更新遮挡
*/
updateBlock() {
this.allCard.forEach((v) => {
v.blocked = false;
});
this.allCard.forEach((a) => {
this.allCard.forEach((b) => {
if (a !== b) {
if (!(a.deleted || b.deleted)) {
if (a.intersect(b)) {
if (a.z > b.z) {
b.blocked = true;
} else {
a.blocked = true;
}
}
}
}
});
});
}
updateView() {
this.setState((s) => {
s.data = this.allCard.map((v) => {
return {
...v,
};
});
});
}
}
- 编写界面
- playground 作为我们的卡片和卡槽区域的容器
- 每张卡片都是通过绝对定位和z-index来控制位置
- 在卡片deleting状态时添加卡片消失动画
- 卡槽也是通过绝对定位来控制位置
import { GamePresenter } from '@/core/game.presenter';
import { usePresenter } from '@clean-js/react-presenter';
import { useEffect } from 'react';
import './index.less';
import 'animate.css';
export default function HomePage() {
const { p, s } = usePresenter(GamePresenter);
useEffect(() => {
p.init();
}, []);
return (
<div className="page">
<div className="playground">
{s.data.map((u) => {
if (!u) {
return null;
}
if (u.deleted) {
return null;
}
return (
<img
onClick={() => {
p.click(u.id);
}}
className={`card ${u.blocked ? 'isBlocked' : ''} ${
u.deleting ? 'animate__animated animate__fadeOut' : ''
}`}
src={u.img}
style={{
left: u.left,
top: u.top,
zIndex: u.z,
width: u.width + 'px',
height: u.height + 'px',
}}
></img>
);
})}
<div
className="slot"
style={{
left: p.state.slot.left,
top: p.state.slot.top,
}}
></div>
<button
type="button"
className="btn1"
onClick={() => {
p.init();
}}
>
重来
</button>
</div>
</div>
);
}
.page {
background-color: green;
width: 100vw;
height: 100vh;
padding: 20px;
}
.playground {
position: relative;
width:200px;
height: 650px;
margin: 20px auto;
}
.card {
position: absolute;
transition: all .6s;
color: rgb(208, 68, 68);
display: flex;
justify-content: center;
align-items: center;
background-color: #fff;
&.isBlocked {
opacity: .3;
}
}
.empty-card {
position: absolute;
transition: all .3s;
}
.slot {
position: absolute;
background: rgba(0, 0, 0, 0.503);
left: 0;
width: 280px;
height: 56px;
}
.btn {
position: absolute;
top: 600px
}
.btn1 {
position: absolute;
top:660px
}
就这样,一个爆款游戏就完成了
在这里使用了clean-js作为状态库来编写游戏,期待宝子们的star⭐️
贴上源码 👉🏻 github.com/lulusir/cle…
整洁架构篇:juejin.cn/post/714092…
clean-js介绍篇: juejin.cn/post/714321…
其他文章
什么?在React中也可以使用vue响应式状态管理
clean-js | 自从写了这个辅助库,我已经很久没有加过班了…
clean-js | 在hooks的时代下,使用class管理你的状态
clean-js | 手把手教你写一个羊了个羊麻将版
写给前端的数据库入门 | 序
写给前端的数据库入门 | docker & 数据库
有没有一种可能,你从来都没有真正理解async
三分钟实现前端写JAVA这件事——装环境
三分钟实现前端写JAVA这件事——VS code