您的位置:首页 > Web前端 > Vue.js

如何使用Vue.js构建存储卡游戏

2020-08-20 15:14 776 查看

If you are new to Vue and want to refresh your basics, this fun exercise will help you build an interesting game.

如果您是Vue的新手,并且想刷新基础知识,那么这个有趣的练习将帮助您构建有趣的游戏。

In this post, I will take your through the step by step process of building a memory card game in VueJS.

在本文中,我将逐步指导您使用VueJS构建存储卡游戏。

Here is what you can expect to learn by the end of this article:

这是您期望在本文结尾处学习的内容:

  • How to use the v-for directive to loop through Array of Objects.

    如何使用v-for指令遍历对象数组。

  • Dynamic class & style binding using the v-bind directive

    使用v-bind指令的动态类和样式绑定

  • How to add Methods and Computed Properties.

    如何添加方法和计算属性。

  • How to add reactive properties to an object using Vue.set

    如何使用Vue.set向对象添加React特性
  • How to use the setTimeout method to delay JavaScript execution.

    如何使用setTimeout方法延迟JavaScript执行。

  • Shallow cloning vs Deep Cloning of Javascript objects.

    浅克隆与深克隆 Javascript对象。

  • How to use the Lodash utility library.

    如何使用Lodash实用程序库。

Let's dive into the steps.

让我们深入到步骤。

准备-包括图书馆 (Getting Ready - Including Libraries)

The first step is simple: just import the libraries from the CDN into our basic HTML5 markup so that we can get started with our tiny little project.

第一步很简单:只需将CDN中的库导入到我们的基本HTML5标记中,就可以开始我们的小项目了。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memory Card Game</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

<!-- development version, includes helpful console warnings -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>

</body>
</html>

允许用户看到卡片网格 (Allow user to see the card grid)

Next up, let's define the necessary HTML markup, CSS styling, and a basic Vue instance so that the user can see the card grid.

接下来,让我们定义必要HTML标记,CSS样式和基本的Vue实例,以便用户可以看到卡片网格。

Vue实例 (Vue Instance)

Let's create a new Vue instance and define the single data property named cards which holds the list of cards.

让我们创建一个新的Vue实例,并定义名为cards的单个数据属性,该属性保存卡片列表。

let app = new Vue({
el: '#app',
data:{
cards: [
{
name: 'Apple',
img: 'apple.gif',

},
{
name: 'Banana',
img: 'banana.gif',

},
{
name: 'Orange',
img: 'orange.jpg',

},
{
name: 'Pineapple',
img: 'pineapple.png',

},
{
name: 'Strawberry',
img: 'strawberry.png',

},
{
name: 'watermelon',
img: 'watermelon.jpg',

},
],
},
});

Each object in the array contains two properties: the name of the image (which will be used to perform matching) and the image of the card.

数组中的每个对象都包含两个属性:图像的名称(将用于执行匹配)和卡的图像。

HTML标记 (HTML MarkUp)

Since we now have the data ready in our Vue instance, we can use the v-for directive in VueJS to loop through it.

由于我们现在已经在Vue实例中准备好数据,因此可以在VueJS中使用v-for指令遍历它。

<div id="app">
<div class="row">
<div class="col-md-6 col-lg-6 col-xl-5 mx-auto">
<div class="row justify-content-md-center">
<div v-for="card in cards" class="col-auto mb-3 flip-container">
<div class="memorycard">
<div class="front border rounded shadow"><img width="100" height="150" src="/assets/images/memorycard/pattern3.jpeg"></div>
<div class="back rounded border"><img width="100" height="150" :src="'/assets/images/memorycard/'+card.img"></div>
</div>
</div>
</div>
</div>
</div>
</div>

We have used some basic Bootstrap markup and the v-for directive of VueJS to loop through the cards and show them in the grid format.

我们使用了一些基本的Bootstrap标记和VueJS的v-for指令来遍历卡并以网格格式显示它们。

Each memory-card is made up of two parts:

每个存储卡均由两部分组成:

  • front: This contains a common pattern image for all the cards (default card view)

    正面:包含所有卡的通用图案图像(默认卡视图)
  • back: This contains the actual card image (needs to be hidden by default)

    背面:包含实际的卡片图片(默认情况下需要隐藏)

Let's add in some basic CSS so that we only show the front part of the card (common design pattern):

让我们添加一些基本CSS,以便仅显示卡的前部(通用设计模式):

.flip-container {
-webkit-perspective: 1000;
-moz-perspective: 1000;
-o-perspective: 1000;
perspective: 1000;
min-height: 120px;
cursor: pointer;
}
.front,
.back {
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
-o-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transition: 0.6s;
-webkit-transform-style: preserve-3d;
-moz-transition: 0.6s;
-moz-transform-style: preserve-3d;
-o-transition: 0.6s;
-o-transform-style: preserve-3d;
-ms-transition: 0.6s;
-ms-transform-style: preserve-3d;
transition: 0.6s;
transform-style: preserve-3d;
top: 0;
left: 0;
width: 100%;
}
.back {
-webkit-transform: rotateY(-180deg);
-moz-transform: rotateY(-180deg);
-o-transform: rotateY(-180deg);
-ms-transform: rotateY(-180deg);
transform: rotateY(-180deg);
position: absolute;
}

Refresh the page and you should see six cards stacked up in the grid format facing the front. The actual card image is hidden on the back.

刷新页面,您应该看到六张卡以面对前的网格格式堆叠。 实际的卡图像隐藏在背面。

让我们翻转卡片 (Let's flip the cards)

Next up, let's bind an event to our cards so that when it's clicked it should flip and show the image behind it.

接下来,让我们将事件绑定到卡片上,以便在单击事件时将其翻转并在其后面显示图像。

Let's modify our original cards array to add another property to each card. This will determine if the card is currently flipped.

让我们修改原始的纸牌数组,为每个纸牌添加另一个属性。 这将确定卡当前是否已翻转。

Add the following CSS. When the flipped class is added to the class it will show the card image. It also gives us a nice turn effect.

添加以下CSS。 将翻转的班级添加到班级时,它将显示卡片图像。 这也给我们带来了不错的转向效果。

.flip-container.flipped .back {
-webkit-transform: rotateY(0deg);
-moz-transform: rotateY(0deg);
-o-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
transform: rotateY(0deg);
}
.flip-container.flipped .front {
-webkit-transform: rotateY(180deg);
-moz-transform: rotateY(180deg);
-o-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
transform: rotateY(180deg);
}

Let's use the Vue created lifecycle event to add the new property and add a flipCard method to flip the card

让我们使用Vue 创建的生命周期事件添加新属性,并添加flipCard方法翻转卡片

created(){
this.cards.forEach((card) => {
card.isFlipped = false;
});
},

methods:{
flipCard(card){
card.isFlipped = true;
}
}

First we'll bind the click event to cards to invoke the flipCard method. Then we'll also use the v-bind directive to bind the flipped class to the card.

首先,我们将click事件绑定到卡上以调用flipCard方法。 然后,我们还将使用v-bind指令将翻转的类绑定到卡上。

...
<div v-for="card in cards" class="col-auto mb-3 flip-container" :class="{ 'flipped': card.isFlipped }" @click="flipCard(card)">
...

Sounds about right – let's see if the cards flip on a click.

听起来不错–让我们看看卡是否在点击时翻转。

It didn't work. Why not?

没用 为什么不?

Let's go back to our created lifecycle method, where we looped through the list of cards and added a new property named isFlipped. It looks alright – but Vue didn't like it.

让我们回到创建的生命周期方法,在其中循环浏览卡片列表并添加了一个名为isFlipped的新属性。 看起来不错–但是Vue不喜欢它。

For the new object properties to be reactive, you have to add them to the object using the Vue.set method.

为了使新的对象属性具有React性,必须使用Vue.set方法将它们添加到对象中。

created(){
this.cards.forEach((card) => {
Vue.set(card,'isFlipped',false)
});
},

Now the cards should flip on click:

现在,卡应该在点击时翻转:

Alrighty, great job. Let's move on to the next one.

好,辛苦了 让我们继续下一个。

加倍并洗牌 (Double it and shuffle it)

Yep, that's right! To make a memory game out of these cards we need to have exactly one pair of each card. We also we need to shuffle the order of the cards every time the game is loaded.

是的,没错! 为了用这些卡制作记忆游戏,我们需要每张卡上恰好一对。 每次加载游戏时,我们还需要洗牌顺序。

Let's define a new property in our Vue instance named memoryCards. Here we will store the cards that will be played (that is, double the amount of actual cards and also shuffled).

让我们在Vue实例中定义一个名为memoryCards的新属性。 在这里,我们将存储将要玩的纸牌(即,将实际纸牌数量翻倍,并随机播放)。

...
memoryCards: [],
...

加倍 (Doubling)

To create two copies of all the cards, let's concatenate the cards array to create and assign it to the memoryCards property.

要创建所有卡的两个副本,让我们串联卡数组以创建并将其分配给memoryCards属性。

Change the v-for directive in the HTML markup to loop over the property memoryCards instead of cards:

更改HTML标记中的v-for指令以遍历属性memoryCards而不是card:

<div v-for="card in memoryCards" class="col-auto mb-3 flip-container" :class="{ 'flipped': card.isFlipped }" @click="flipCard(card)">

Next, modify the lifecycle method created to assign the concatenated array into memoryCards:

接下来,修改创建的生命周期方法以将串联的数组分配给memoryCards:

created(){
this.cards.forEach((card) => {
Vue.set(card,'isFlipped',false)
});

var cards1 = this.cards;
var cards2 = this.cards;
this.memoryCards = this.memoryCards.concat(cards1, cards2);
},

Looks simple, right?

看起来很简单,对吧?

But this isn't gonna work correctly. There are two problems with this code:

但这无法正常工作。 此代码有两个问题:

  1. Direct assignment of this.cards into cards1 is not going to make another copy of cards object. cards1 is still referencing the original object.

    直接将this.cards分配到cards1不会使cards对象成为另一个副本。 cards1仍在引用原始对象。
  2. Since cards1 and cards2 are still referencing same object this means that we have concatenated two arrays which point to same array of objects.

    由于cards1和cards2仍​​在引用同一对象,这意味着我们已将指向同一对象数组的两个数组连接在一起。

Changing any property of the object in the memoryCards object will change the original array as well as its own pair in the array.

更改memoryCards对象中对象的任何属性将更改原始数组以及该数组中它自己的对。

Well, that's a problem.

好吧,那是个问题。

If you look around for solutions to properly copy an array or object so that it doesn't refer to the original array, you might come across solutions that do a shallow-copy of the array.

如果四处寻找可以正确复制数组或对象的解决方案,以使其不引用原始数组,则可能会遇到对数组进行浅表复制的解决方案。

什么是浅表副本? (What's a shallow copy?)

A shallow copy refers to the fact that only one level is copied. That will work fine for an array or object containing only primitive values.

浅表副本是指仅复制一个级别的事实。 对于仅包含原始值的数组或对象,这将很好地工作。

One way to do shallow-copy is via the spread operator, which in our case will be something like the below code:

进行浅表复制的一种方法是通过散布运算符,在我们的情况下,它将类似于以下代码:

...
var cards1 = [...this.cards];
var cards2 = [...this.cards];
this.memoryCards = this.memoryCards.concat(cards1, cards2);
...

But this is not the solution for us, because in our case we have an array of objects and not of any primitive values. Thus our problem can be solved if we do a deep copy of our array.

但这不是我们的解决方案,因为在我们的情况下,我们有一个对象数组,没有任何原始值。 因此,如果我们对数组进行深层复制,则可以解决我们的问题。

什么是深层副本? (What's a deep copy?)

For objects and arrays containing other objects or arrays, copying these objects requires a deep copy. Otherwise, changes made to the nested references will change the data nested in the original object or array.

对于包含其他对象或数组的对象和数组,复制这些对象需要深度复制。 否则,对嵌套引用的更改将更改嵌套在原始对象或数组中的数据。

There are multiple ways of doing a deep copy, but we'll go with the simplest and most common way of using the Lodash library.

进行深层复制有多种方法,但是我们将使用使用Lodash库的最简单,最常见的方法。

Now, whats the Lodash library?

现在,什么 Lodash库?

Lodash makes JavaScript easier by taking the hassle out of working with arrays, numbers, objects, strings, etc.

Lodash消除了处理数组,数字,对象,字符串等的麻烦,从而使JavaScript变得更容易。

For our case Lodash has a method to perform deepCopy which makes it ridiculously simple.

对于我们的情况,Lodash有一种执行deepCopy的方法,这使其非常简单。

First include Lodash in your page by either downloading or referencing it through the CDN.

首先,通过CDN下载或引用Lodash将Lodash包含在页面中。

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script>

Next, you can use Lodash's cloneDeep method to perform the deep copy of our cards array.

接下来,您可以使用Lodash的cloneDeep方法执行卡片阵列的深层复制。

var cards1 = _.cloneDeep(this.cards);
var cards2 = _.cloneDeep(this.cards);
this.memoryCards = this.memoryCards.concat(cards1, cards2);

改组 (Shuffling)

Now we want to shuffle the concatenated array. Lodash has a method to shuffle as well. Let's use the method and also simplify the code to concatenate and shuffle in a single line.

现在,我们想改组连接的数组。 Lodash也有洗牌的方法。 让我们使用该方法,并简化代码以在一行中进行串联和混洗。

created(){
this.cards.forEach((card) => {
Vue.set(card,'isFlipped',false)
});

this.memoryCards = _.shuffle(this.memoryCards.concat(_.cloneDeep(this.cards), _.cloneDeep(this.cards)));
},

Cards are now shuffling and flipping as expected.

卡现在像预期的那样洗牌和翻转。

On to the next thing!

接下来的事情!

配套卡 (Matching Cards)

The next step is to match the flipped cards. A user is allowed to flip a maximum of two cards at a time. If they are same, its a match! If they are not, then we flip them back.

下一步是匹配翻转的卡。 允许用户一次最多翻转两张卡。 如果它们相同,那就匹配了! 如果不是,那么我们将它们翻转回去。

Let's tackle this.

让我们解决这个问题。

We'll add a new property to each card to track if the card has already been matched. Modify the created method to include this code:

我们将为每个卡添加一个新属性,以跟踪该卡是否已匹配。 修改创建的方法以包括以下代码:

this.cards.forEach((card) => {
Vue.set(card,'isFlipped',false);
Vue.set(card,'isMatched',false);
});

Create a new data property to store the flipped cards:

创建一个新的数据属性来存储翻转的卡:

flippedCards: [],

Next up, we modify the flipCard method to perform matching:

接下来,我们修改flipCard方法以执行匹配:

flipCard(card){
card.isFlipped = true;

if(this.flippedCards.length < 2)
this.flippedCards.push(card);
if(this.flippedCards.length === 2)
this._match(card);
},

_match(card){

if(this.flippedCards[0].name === this.flippedCards[1].name)
this.flippedCards.forEach(card => card.isMatched = true);
else
this.flippedCards.forEach(card => card.isFlipped = false);

this.flippedCards = [];
},

The logic here is simple: we keep adding cards to the flippedCards array until there are two cards.

这里的逻辑很简单:我们不断将卡片添加到flippedCards数组中,直到有两张卡片为止。

Once there are two cards, we perform matching.

一旦有两张卡,我们执行匹配。

  • If the name of both the cards is the same, we mark the cards as matched by setting the isMatched property to true.

    如果两个卡的名称相同,则通过将isMatched属性设置为true来将卡标记为匹配。
  • Else, we set the isFlipped property back to false.

    否则,我们将isFlipped属性设置回false。

We clear out the flippedCards array after this.

在此之后,我们清除了flippedCards数组。

Add a new CSS property to fade out the cards that match:

添加一个新CSS属性以淡出匹配的卡片:

.matched{
opacity: 0.3;
}

Add a class binding to the container to add matched cards if the property is set to true:

如果属性设置为true,则向容器添加类绑定以添加匹配的卡:

:class="{ 'flipped': card.isFlipped, 'matched' : card.isMatched }"

Here the logic works fine, but everything happens too fast for the player to understand whats going on. If the cards don't match they are flipped back even before the user can see the revealed card.

此处的逻辑工作正常,但是一切发生得太快,以至于玩家无法理解发生了什么。 如果卡片不匹配,它们甚至会在用户看到显示的卡片之前向后翻转。

Let's use the setTimeout method of JavaScript to add a deliberate delay of few microseconds.

让我们使用JavaScript的setTimeout方法来添加几微秒的故意延迟。

_match(card){
if(this.flippedCards[0].name === this.flippedCards[1].name){
setTimeout(() => {
this.flippedCards.forEach(card => card.isMatched = true);
this.flippedCards = [];
}, 400);
}
else{
setTimeout(() => {
this.flippedCards.forEach((card) => {card.isFlipped = false});
this.flippedCards = [];
}, 800);
}
},

We added 400 microseconds of delay before marking them as matched, and 800 microseconds to delay before flipping them back.

在将它们标记为匹配之前,我们增加了400微秒的延迟,而在将它们翻转回之前增加了800微秒的延迟。

Also modify the flipCard method to not flip the cards when

还要修改flipCard方法,以在以下情况下不翻转卡片

  • Card is already matched

    卡已匹配
  • Card is already flipped

    卡已被翻转
  • User has already flipped two cards

    用户已经翻转了两张卡
flipCard(card){

if(card.isMatched || card.isFlipped || this.flippedCards.length === 2)
return;

card.isFlipped = true;

if(this.flippedCards.length < 2)
this.flippedCards.push(card);
if(this.flippedCards.length === 2)
this._match(card);
},

We are almost there, just few more steps.

我们快到了,只需要几个步骤。

完成游戏 (Finish the Game)

The game is marked as finished when all the cards are matched.

当所有纸牌都匹配时,游戏被标记为完成。

Let's quickly write the code condition for that. We introduce a new data property in our Vue instance:

让我们快速编写代码条件。 我们在Vue实例中引入了一个新的data属性:

...
finish: false

Next, we modify the match method to check if all cards are matched after every successful match.

接下来,我们修改匹配方法,以在每次成功匹配后检查所有卡是否都匹配。

setTimeout(() => {
this.flippedCards.forEach(card => card.isMatched = true);
this.flippedCards = [];

//All cards matched ?
if(this.memoryCards.every(card => card.isMatched === true)){
this.finish = true;
}

}, 400);

We use the every method of JavaScript arrays which evaluates the given condition for truth, if not it returns false.

我们使用JavaScript数组的every方法,该方法会评估给定条件是否为真,否则返回false。

跟踪总转弯和总时间 (Keep Track of Total Turns and Total Time)

We have built the game, so now let's make it more interesting by giving it some finishing touches. We will add how many turns a user has taken, and also how they are doing on time taken to complete the game.

我们已经构建了游戏,所以现在让我们通过一些画龙点睛的使其变得更有趣。 我们将添加用户进行了多少回合,以及他们如何按时完成游戏。

First we'll introduce some new data properties:

首先,我们将介绍一些新的数据属性:

start: false
turns: 0,
totalTime: {
minutes: 0,
seconds: 0,
},

Once there are two cards flipped we will increase the count. Thus we'll modify the _match method to increment the turns.

一旦翻转了两张卡,我们将增加计数。 因此,我们将修改_match方法以增加转弯。

...
_match(card){

this.turns++;

...

Next up we modify the flipCard method to start the timer:

接下来,我们修改flipCard方法以启动计时器:

flipCard(card){

if(card.isMatched || card.isFlipped || this.flippedCards.length === 2)
return;

if(!this.start){
this._startGame();
}

...
...

Add two new methods to start the clock once the game is started:

游戏开始后,添加两种新方法来启动时钟:

_startGame(){
this._tick();
this.interval = setInterval(this._tick,1000);
this.start = true;
},

_tick(){
if(this.totalTime.seconds !== 59){
this.totalTime.seconds++;
return
}

this.totalTime.minutes++;
this.totalTime.seconds = 0;
},

We use computed properties to pad up a '0' in front of minutes and seconds when they are single digits:

当它们是个位数时,我们使用计算属性在分钟和秒的前面填充“ 0”:

computed:{
sec(){
if(this.totalTime.seconds < 10){
return '0'+this.totalTime.seconds;
}
return this.totalTime.seconds;
},
min(){
if(this.totalTime.minutes < 10){
return '0'+this.totalTime.minutes;
}
return this.totalTime.minutes;
}
}

Add the following HTML just above your HTML to display the total number of turns and total time:

在HTML上方添加以下HTML,以显示总匝数和总时间:

<div class="d-flex flex-row justify-content-center py-3">
<div class="turns p-3"><span class="btn btn-info">Turns : <span class="badge" :class="finish ? 'badge-success' : 'badge-light'">{{turns}}</span> </span></div>
<div class="totalTime p-3"><span class="btn btn-info">Total Time : <span class="badge" :class="finish ? 'badge-success' : 'badge-light'">{{min}} : {{sec}}</span></span></div>
</div>

Modify the finish game condition to stop the timer once the game is finished:

修改完成游戏条件以在游戏结束后停止计时器:

if(this.memoryCards.every(card => card.isMatched === true)){
clearInterval(this.interval);
this.finish = true;
}

重启 (Reset)

We are at our last step – good job if you've made it to this point.

我们已经走到了最后一步-如果您做到了这一点,那就很好。

Let's add a button to reset the game:

让我们添加一个按钮来重置游戏:

<div class="totalTime p-3"><button class="btn btn-info" @click="reset" :disabled="!start">Restart</button></div>

Bind the click event to the reset method:

将click事件绑定到reset方法:

reset(){
clearInterval(this.interval);

this.cards.forEach((card) => {
Vue.set(card, 'isFlipped',false);
Vue.set(card, 'isMatched',false);
});

setTimeout(() => {
this.memoryCards = [];
this.memoryCards = _.shuffle(this.memoryCards.concat(_.cloneDeep(this.cards), _.cloneDeep(this.cards)));
this.totalTime.minutes = 0;
this.totalTime.seconds = 0;
this.start = false;
this.finish = false;
this.turns = 0;
this.flippedCards = [];

}, 600);

},

We clear out the timer, reshuffle the cards, and reset all the fields back to their default value.

我们清除计时器,重新洗牌,然后将所有字段重置为默认值。

We also modify the created lifecycle method to call the reset method to avoid code duplication:

我们还修改了创建的生命周期方法以调用reset方法,以避免代码重复:

created(){
this.reset();
},

There you go ! You now have a memory game in VueJS.

你去! 您现在在VueJS中有一个记忆游戏。

If you are looking to learn VueJS basics along with such fun practice exercises, you can read the VueJS Tutorial series on my personal blog 5Balloons VueJS Course.

如果您想学习VueJS基础知识以及此类有趣的练习,可以在我的个人博客5Balloons VueJS课程中阅读VueJS教程系列。

翻译自: https://www.freecodecamp.org/news/how-to-build-a-memory-card-game-with-vuejs/

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: