網頁遊戲 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/網頁遊戲/ 蒐集互動設計案例、教學與業界資源,幫助你一起進入互動程式創作的產業 Tue, 17 Aug 2021 13:47:14 +0000 zh-TW hourly 1 https://wordpress.org/?v=6.2.2 https://creativecoding.in/wp-content/uploads/2022/03/cropped-cct-logo-icon-2-32x32.png 網頁遊戲 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/網頁遊戲/ 32 32 Vue.js入門:英文2000字互動遊戲網頁 https://creativecoding.in/2021/08/26/vue-js-%e8%8b%b1%e6%96%872000%e5%ad%97%e4%ba%92%e5%8b%95%e9%81%8a%e6%88%b2%e7%b6%b2%e9%a0%81/ Thu, 26 Aug 2021 01:38:00 +0000 https://creativecoding.in/?p=1397 你多久沒鍛鍊英文了呢?這次直播使用HTML(Pug)CSS(Sass)以及Javascript(Vue.js)來完成一個練習2000字英文單字的小互動遊戲網頁,難度適中,尤其適合剛開始學習Vue.js的新手,無論是跟著步驟逐步操作,或是聽老闆的影片都能快速完成。

這篇文章 Vue.js入門:英文2000字互動遊戲網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
英文2000字選擇題互動網頁完成圖
英文2000字選擇題互動網頁完成圖

這次直播的主題來自於老闆在2018年印象清華-物聯網科技藝術展中創作的展品〈英文8-2〉,其概念為十根代表清大不同學院的光柱,使用者只要掃描光柱上的QR Code就會跳出互動式的英文單字題目,只要答對題數越多、分數就越多,累積的分數便會即時地投射到光柱上,形成高高低低、動態交錯的有趣光景。

在這次直播中會來聊些這個專案內使用Vue的相關經驗,聊聊製作互動裝置藝術實作時整合的各種辛酸血淚史,以及如何快速地解析別人資料,利用Vue框架製作成幫你找回國中逝去英文能力的遊戲。

我們將以Code Pen做為本次實作的平台,這是一個可以在創作的當下即時看到程式碼運作狀況的線上程式碼編輯器,只要簡單註冊就可以使用囉!

如果想搭配直播影片一起實作,請往這邊走 👉🏻 https://www.youtube.com/watch?v=maFbo96YT8U

前期準備

在開始之前我們根據概念來進行規劃,想像一個英文單字的互動答題App需要哪些東西:

  1. 整理網路上現成的單字表,把資料變成符合我們條件的JSON格式物件,單字表必須同時具備英文、中文與詞性(今天借用的是109英文銜接教材2000單字
  2. 產生隨機的英文題目,並利用整理後的物件選出正確答案和其他類似的詞當作選項,並判斷答題者的正確與否
  3. 如果答題者正確,跟後端同步狀態累加分數

首先在Code Pen上開一個新的pen,將HTML的預處理器設定成Pug、CSS的預處理器設定成Sass、JS的CDN掛入Vue。

程式環境設定
程式環境設定

接著把單字表上的文字複製下來貼到Javascript中,var一個a,並且用ES6的頓號 ` 把文字包起來。

//Javascript
var a = `
A
able adj. 有能力的
about prep. 有關
above adv. 在上方
`

註:考慮篇幅關係這邊只貼上部分A字首的單字,實際資料請參考單字表。

我們快速分析一下他的架構組成,參考字首A的部分得知單字表主要可以分為:字首的段落開頭、英文、詞性、中文,也就是──只要是沒有英文單字的那一行就不會有「.」,如果說我們今天要把單字整理成一個一個的物件時,可以把每一行先分割出來、把含有「.」的留下,再分別拆解成英文、詞性、中文,這就是我們所需的資料。

1. 拆解單字表

利用語法split以空行來做分隔,再用語法filter把含有.的行過濾保留下來,利用list.lengthlist[n]在console查看過濾後的listlist2數量上的差異,代表我們的資料越來越乾淨了。利用語法map把原先陣列的一行一行轉化成一個一個,再存成另一個陣列,轉換的條件為用空格分割,console會發現list3裡面裝著一坨拉庫的[object Array](3)。再把list3拆分成wordcatatrans,分別對應英文、詞性、中文的物件。

//Javascript
var list = a.split("\n") //分割空行
var list2 = list.filter(item=>item.indexOf(".")!=-1) //過濾沒有.
var list3 = list2.map(item=>item.split(" ")) //單行轉單個
var list4 = list3.map(item=>({
  english: item[0],
  cata: item[1],
  trans: item[2]
})) //拆分成英文、詞性、中文

資料搬運小幫手Vue

Vue的特色在於資料雙向綁定,相較於jQuery需要選取物件、重新定義、再塞回去以直接操作 DOM 物件為主的方式,利用Vue的同步更新渲染資料可以幫助我們節省不少時間。Vue的寫法為在JS透過 new Vue建立作用範圍和待即時同步的資料,同時在HTML以{{}}包裹被更新的變數。以下為官方網站所舉的範例,Vue會將大括號{{}}的內容對應到message狀態,並且將之即時渲染至畫面上,也就是所指的「宣告式渲染」。

//HTML
<div id="app">
  {{ message }}
</div>
//Javascript
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

延伸閱讀: 「Vue.js 學習筆記 Day1」- 建立 Vue 應用程式 重新認識 Vue.js | Kuro Hsu

2. 實際運用Vue-單字小卡

運用Vue和剛剛整理好的單字表資料來試做一些英文小卡吧!取出list4中倒數20個單字用v-for迭代陣列中的物件,指定其資料種類並渲染在li的span,再給一些CSS的參數後就可以看到一張張排列整齊的你國中的惡夢單字小卡。透過Vue的幫忙,我們不用自己產生元件跟呈現,只需要確定資料是否正確即可。

//HTML
#app 
    h2 我的名字是{{name}}
    ul
      li(v-for="word in words")
        span {{word.english}} {{word.cata}} {{word.trans}} //指定word中的種類
//CSS
html,body
  background-color: #222

ul
  li
    background-color: #fff
    padding: 20px
    display: inline-block
    margin: 20px
    width: 200px
//Javascript
var vm = new Vue({
  el: '#app',
  data: {
    name: "Frank",
    catas: ["a","b","c","d"],
    words: list4.slice(-20) //負號代表從後面數來的20個單字
  }
})
英文2000字選擇題互動網頁:步驟二,製作出單字卡
英文2000字選擇題互動網頁:步驟二,製作出單字卡

3. 製作答題選項

製作完單字小卡有沒有覺得長得很像我們的答題選項呢?在Vue中我們定義methods為操作不同 DOM 元素的方法,這邊需要綁定幾個動作:

  1. click DOM元素根據滑鼠點擊的動作,在console回傳所點擊的單字。
  2. getOptions挑選同詞性、同字首的隨機四個單字。用filter過濾word.cata == question.cata也就是詞性需與答案相同,過濾第二次word.english[0] == question.english[0]英文單字的字首(第0個字)需相同,過濾第三次word.english !== question.english確認答案不會等於題目。
  3. 使用sort把陣列的順序打亂:.sort((a,b)⇒a-b)是將大的往後排,但sort((a,b)=>Math.random())則是隨機取值,再扣掉比較函數0.5後成為真正隨機排序的陣列,加上.slice(0,4)限縮在一次只取四個單字。
  4. .sort前面加入第二個.slice()把原本的元素複製一份成新的陣列,避免影響到既有陣列的順序,雖然針對新的陣列動屬性依然會影響原先的物件,但兩個陣列的順序是分開的。
  5. 亂數打亂正確答案的位置,目前都是把正確答案推到最前面,取得result後用concat連接question這個陣列,再打亂一次排序,成為result2
//HTML
#app 
  h2 我的名字是{{name}}
  ul
    li(v-for="word in words", v-on:click="click(word.english)") //讓console顯示出滑鼠點擊到哪個英文單字
      span {{word.english}} {{word.cata}} {{word.trans}}
//Javascript
var vm = new Vue({
  el: '#app',
  data: {
    name: "Frank",
    catas: ["a","b","c","d"],
    words: list4
  },
  methods:{
    click(word){
      console.log("click",word)
    },
    getOptions(question){
      let result = this.words.filter(
      word => word.cata == question.cata).filter(
      word => word.english[0] == question.english[0]).filter(
      word => word.english !== question.english
      ).slice().sort((a,b)=>Math.random()-0.5).slice(0,4)
      let result2 = result.concat([question]).slice().sort((a,b)=>Math.random()-0.5)
      return result2
    }
  }
})

註:concat只能做陣列與陣列的連接。

4. 建立一個出題目按鈕,產生新題目

methods新增pick(),在data中定義還沒開始之前question: null,用this存取本身的資料屬性,隨機選取陣列裡的其中一個字,記得因為index須為整數所以加上parseInt

//HTML
#app 
  button(@click="pick") 出題囉
  h2(v-if="question") {{ question.english }}
//CSS
html,body
  background-color: #222
  color: #fff
//Javascript
var vm = new Vue({
  el: '#app',
  data: {
    ...
    question: null
  },
  methods:{
    click(word){
      console.log("click",word)
    },
    pick(){
      this.question = this.words[parseInt(Math.random()*this.words.length)]
    },
    getOptions(question){
      ...
    }
  }
})
英文2000字選擇題互動網頁:步驟四,出題按鈕
英文2000字選擇題互動網頁:步驟四,出題按鈕

5. 建立選項

把單字的中文抓出來印成題目,同時也把選項抓出來存取,一開始會是空的陣列所以在data定義options: [],再在pick()中多加一行程式碼把產生的新題目裝回去。

//HTML
#app 
  button(@click="pick") 出題囉
  h2(v-if="question") {{ question.trans }}
    ul
      li(v-for="option in options") {{option.english}}
//CSS中要先把li的樣式暫時註解掉
//Javascript
var vm = new Vue({
  el: '#app',
  data: {
    ...
    question: null,
    options: []
  },
  methods:{
    click(word){
      ...
    },
    pick(){
      this.question = this.words[parseInt(Math.random()*this.words.length)]
      this.options=this.getOptions(this.question)
    },
    ...
  }
})
英文2000字選擇題互動網頁:步驟五,建立選項
英文2000字選擇題互動網頁:步驟五,建立選項

6. 判斷答案正確與否

比較簡單的做法是在產生資料時同時附加他是否正確的資訊在其中,我們複製一份新的question避免影響原本的,在let result2前面加上let questionClone = JSON.parse(JSON.stringify(question)),再把帶有正確與否屬性的物件混到原有的選項中,把questionClone作為判斷的正確答案,而result2中原本的question也要記得替換成questionClone

methods新增check(option),點擊選項時如果正確,console印出correct、不正確則印出wrong,回答完後再重新出題this.pick()

//HTML
...
ul
  li(v-for="option in options",
     @click="check(option)") {{option.english}}
//Javascript
...
  methods:{
    click(word){
      console.log("click",word)
    },
    check(option){
      if (option.correct){
        console.log("correct")
      }else{
        console.log("wrong")
      }
      this.pick() //點選答案無論對錯都會換下一題
    },
    pick(){
      ...
    },
    getOptions(question){
      let result = this.words.filter(
      word => word.cata == question.cata).filter(
      word => word.english[0] == question.english[0]).filter(
      word => word.english !== question.english
      ).slice().sort((a,b)=>Math.random()-0.5).slice(0,4)
      let questionClone = JSON.parse(JSON.stringify(question))
      questionClone.correct=true
      let result2 = result.concat([questionClone]).slice().sort((a,b)=>Math.random()-0.5)
      return result2
    }
  }
})
英文2000字選擇題互動網頁:步驟六,console顯示出選擇了正確或錯誤答案
英文2000字選擇題互動網頁:步驟六,console顯示出選擇了正確或錯誤答案

7. 增加答題分數計算機制以及顯示正確或錯誤

要增加答題分數grade的計算機制,首先在data中定義grade: 0,在check(option)中如果答對了就加一分this.grade++,並在HTML中顯示。

//HTML
#app 
  h3 Score:{{grade}}
  ...
//Javascript
...
  data: {
    ...
    grade: 0
  },
  methods:{
    click(word){
      ...
    },
    check(option){
      if (option.correct){
        console.log("correct")
        this.grade++
      }else{
        console.log("wrong")
      }
      this.pick()
    },
    pick(){
      ...
    }
  }

目前答題正確與否只能靠分數是否有增加得知,要改成更直觀一點,點擊選項時如果正確,在題目右邊會印出correct並累加分數this.grade++、不正確則印出wrong,我們使用一個預設是空字串的變數status去儲存這個資訊,status設定過1秒後消失,回答完後再重新出題this.pick()

通常使用者在進到介面時題目已經出好了,答題後會自動更新,所以頁面剛載入時便自動執行一次pickmounted代表Vue已經準備好可以幫忙計算資料了,所以跟methodsdatael在同一層級。

//HTML
#app 
  .container
    .row
      .col-sm-12
        h3 Score:{{grade}}
        h2(v-if="question") Q: {{question.trans}}
          .status {{status}}
        ul 
          li(v-for="option in options", @click="check(option)") {{option.english}}
//Javascript
var vm = new Vue({
  el: '#app',
  data: {
    name: "Frank",
    catas: ["a","b","c","d"],
    words: list4,
    question: null,
    options: [],
    status: "",
    grade: 0
  },
  mounted(){
    this.pick()
  },
  methods:{
    click(word){
      console.log("click",word)
    },
    check(option){
      if (option.correct){
        this.status =("correct")
        this.grade++
      }else{
        this.status = ("wrong")
      }
      setTimeout(()=>{
        this.status=""
        this.pick()
      },1000)
      this.pick()
    },
    pick(){
      this.question = this.words[parseInt(Math.random()*this.words.length)]
      this.options = this.getOptions(this.question)
    },
    getOptions(question){
      let result = this.words.filter(
      word => word.cata == question.cata).filter(
      word => word.english[0] == question.english[0]).filter(word => word.english !== question.english).slice().sort((a,b)=>Math.random()-0.5).slice(0,4)
      let questionClone = JSON.parse(JSON.stringify(question))
      questionClone.correct = true
      let result2 = result.concat([questionClone]).sort((a,b)=>Math.random()-0.5)
      return result2
    }
  }
})
英文2000字選擇題互動網頁:步驟七,增加答題對錯的顏色回饋

8. 設計畫面及加入動畫

完成骨幹後我們來稍微美化一下吧!在codepen設定CSS的地方引入Bootstrap和Animate.css,把DOM元素放到container內,讓版面豐富一些可以加入hover的滑鼠互動效果和答對或答錯時相對應的變色效果。

變色效果我們可以利用Vue的特性來製作──透過判斷式給予class,也就是判斷當前的status為correct或wrong,如果是correct則顯示綠色、wrong則顯示橘色,必須注意的是由於其他四個錯誤答案的status同時都會是wrong,所以要多下一個判斷點currentOption記錄只有點選的這個選項是wrong時才顯示橘色。在data定義currentOption: {}methodscheck(option)的加入條件this.currentOption = option

英文2000字選擇題互動網頁:步驟八,增加答題對錯的顏色回饋
英文2000字選擇題互動網頁:步驟八,增加答題對錯的顏色回饋

淡入的效果我們則透過Animate.css和Vue的key來製作,每次物件重新產生時都帶有新的english值,所以我們給予的key值也會不一樣,這樣他的animated.fadeIn效果就會重新被載入。

//HTML
#app 
  .container
    .row
      .col-sm-12
        h3 Score:{{grade}}
        h2.animated.fadeIn(v-if="question",:key="question.english") Q: {{question.trans}}
          .status {{status}}
        ul.animated.fadeIn(:key="question.english")
          li(v-for="option in options", @click="check(option)",:class="{correct: status=='correct'&& option.correct, error:status=='wrong' && currentOption.english==option.english}") {{option.english}}
//CSS
ul
  list-style: none
  padding: 0
  li
    padding: 10px
    margin-top: 20px
    border: 1px solid white
    cursor: pointer
    font-size: 30px
    transition: .5s
    &:hover
      background-color: rgba(white,0.1)
    &.correct
      background-color: #38d138
    &.error
      background-color: #ff7332
      
.status 
  float: right
//Javascript
...
var vm = new Vue({
  el: '#app',
  data: {
    name: "Frank",
    catas: ["a","b","c","d"],
    words: list4,
    question: null,
    currentOption: {}, //給予{}以防報錯
    options: [],
    status: "",
    grade: 0
  },
  mounted(){
    this.pick()
  },
  methods:{
    click(word){
      console.log("click",word)
    },
    check(option){
      this.currentOption = option //加入條件
      if (option.correct){
        this.status =("correct")
        this.grade++
      }else{
        this.status = ("wrong")
      }
      setTimeout(()=>{
        this.status=""
        this.pick()
      },1000)
    },
    pick(){
      this.question = this.words[parseInt(Math.random()*this.words.length)]
      this.options = this.getOptions(this.question)
    },
    getOptions(question){
      let result = this.words.filter(
      word => word.cata == question.cata).filter(
      word => word.english[0] == question.english[0]).filter(word => word.english !== question.english).slice().sort((a,b)=>Math.random()-0.5).slice(0,4)
      let questionClone = JSON.parse(JSON.stringify(question))
      questionClone.correct = true
      let result2 = result.concat([questionClone]).sort((a,b)=>Math.random()-0.5)
      return result2
    }
  }
})

以上就是這次的英文2000字即時互動小遊戲網頁的製作介紹拉,這次講解了許多Vue的基礎概念與用法,如果是剛開始接觸Vue的朋友很適合拿來小練身手唷!我們下次再見啦~

成品請參考這邊 👉🏻 https://codepen.io/frank890417/pen/RygVde

英文2000字選擇題互動網頁成果

重點回顧:

  1. 利用filter()、sort()、slice()整理與排序資料
  2. 宣告式渲染的使用方式,資料與function的對應關係
  3. 動態判斷class製作css animation效果

動畫互動網頁程式入門(HTML/CSS/JS)以簡單例子帶你入門網站的基礎架構及開發,用素材刻出簡單有趣又美觀的網頁和動畫,享受做出獨一無二的網頁所帶來的成就感,在職場上與設計師和工程師合作無間。

打好基本的互動網頁基礎之後,可以進階動畫互動網頁特效入門(JS/CANVAS),紮實掌握JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。長達3085分鐘,超過60個精緻範例與400張的投影片以上,以及四個加碼單元vue-cli、GSAP、D3、Three.js的投影片,成為hahow上最長的課程。

此篇直播筆記由幫手 Jeudi Kuo 協助整理

墨雨設計banner

這篇文章 Vue.js入門:英文2000字互動遊戲網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
Vue.js入門:完成懷舊的井字圈叉遊戲動態網頁 https://creativecoding.in/2021/08/10/vue-js-%e6%87%b7%e8%88%8a%e7%9a%84%e4%ba%95%e5%ad%97%e5%9c%88%e5%8f%89%e9%81%8a%e6%88%b2%e5%8b%95%e6%85%8b%e7%b6%b2%e9%a0%81/ Tue, 10 Aug 2021 03:33:00 +0000 https://creativecoding.in/?p=1354 小時候都玩過圈圈叉叉的遊戲,隨手畫個井字就可以與玩伴鬥智,這次我們要利用Pug、Sass及Vue.js製作出這款動態小遊戲網頁啦!

這篇文章 Vue.js入門:完成懷舊的井字圈叉遊戲動態網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
想要探索 Vue 前端框架嗎?本次的圈圈叉叉範例可以帶你一同進入 Vue 世界中,認識前端框架的強大與方便之處。圈圈叉叉是我們每個人小時候都有玩過的遊戲,規則簡單易上手,在本次範例中會實作出以下功能:

  1. 透過 Vue 產生九宮格的框框,當玩家點擊時會顯示出圈圈叉叉
  2. 遊戲過程中系統會不斷比對資料,找尋是否有贏家產生,當有贏家產生時會顯示出哪一方勝利
  3. 可將遊戲畫面清除,重新開始進行一局新的遊戲
  4. 顯示當下該輪到哪一位玩家進行出手

在開始製作之前你該知道的 Js 操作與 Css 屬性

Js操作:

  • filter:篩選出符合贏家條件的資料
  • map:處理九宮格資料時的陣列的轉換
  • reduce:可將數字相加後,用於贏家判斷

Css排列版面的利器flex

  • display: flex:預設會將子層水平排列不斷行
  • flex-wrap: nowrap | wrap | wrap-reverse 預設為 nowrap,超過寬度的子元素是否換行
  • flex-direction: row | row-reverse | column | column-reverse 主軸線的元素排列方向,預設為橫向順排 row
  • justify-content: 元素在主軸上排列的方式
  • align-items: 元素在副軸上排列的方式

事前準備

Code Pen上開一個新的pen,將HTML的預處理器設定成Pug、CSS的預處理器設定成Sass、JS的CDN掛入Vue。

1. 用CSS建立外框線以及圈圈圖示

首先建立名稱為 size 的 mixin,可以讓後續在使用上比較方便,像是繪製框線。

//HTML
.block
//CSS
@mixin size($w, $h:$w)
  width: $w
  height: $h

.block
  +size(150px)
  border: solid 1px

接著要來建立框線以及繪製圈圈的圖示,在繪製圈圈圖示上,會使用擬元素 beforeafter,這裡可以嘗試在 content 中加上符號來顯示看看效果。不過如果沒有要顯示字元的話,像是本次範例所要的是畫幾何圖形,一樣需要加上 content,雙引號中間空白無文字,不可直接省略。

//HTML
.block
.block.circle
//CSS
@mixin size($w, $h:$w)
  width: $w
  height: $h

.block
  +size(150px)
  border: solid 1px
  &.circle
    &:after, &:before
      content: ""
      display: block

不管是圈圈與叉叉,都是由 :after:before 這兩個偽元素左組成,在圈圈上,是兩個圓形疊在一起所產生視覺效果,透過將中間的圓圈所設定的顏與背景顏色相同,這樣看上去可產生甜甜圈形狀也就是圓形的效果。

這邊要注意的是,由於在圖層的排列順序上,:after 是在 :before 之上,所以比較大的圓是要寫在 :before 上,而中間的小圓則是寫在 :after 上。

在圓形的置中設定上,先將 :after:before 設定成絕對定位,而由於絕對定位的位置規則是會透過尋找上層非 static 的區塊,當作定位的起始點。為了使 .circle 當成定位起的點,所以在母元素 .circle 加上相對定位 position: relative。接著以左上角為參考點,向右以及向下各移動 50% ,再向上以及向左移動本身一半的長度 (transform: translate(-50%, -50%)),這樣一來就完成置中了。

//CSS
$color_blue: #46f
$color_red: #f35
$color_bg: #222

@mixin size($w, $h:$w)
  width: $w
  height: $h

html, body
  background-color: $color_bg
  margin: 0
  +size(100%)

.block
  +size(150px)
  border: solid 1px

  &.circle
    position: relative
    &:after, &:before
      content: ""
      display: block
      border-radius: 50%
      position: absolute
      left: 50%
      top: 50%
      transform: translate(-50%, -50%)
    &:before
      +size(90%)
      background-color: $color_red
    &:after
      +size(60%)
      background-color: $color_bg
井字圈叉遊戲:步驟一,建立外框線及圈圈
井字圈叉遊戲:步驟一,建立外框線及圈圈

2. 繼續使用CSS完成叉叉圖示

完成圈圈後,接著來做叉叉。

為了測試方便,所以就先以滑鼠移入區域,也就是 :hover 的方式來寫叉叉的效果。叉叉是由兩條長方形所組成的,所以將 size 的比例改成長方形,也將圓角設定變為 0px。

將長方形個別旋轉正負 45 度,形成交錯的叉叉,這邊可以發現,除了旋轉 (rotate) 之外,也保留了在圓形中所設定的移動 (translate)。原因在於,transform 會複寫掉前面的屬性,為了達到與圈圈一樣的效果,所以也必須指定移動的效果。

此時滑鼠移入有叉叉的效果了,但是切換時很生硬,要達成柔順切換的效果,只要在 &:after, &:before 上加上transition: 0.5s 就可以了。

//CSS
&:after, &:before 
  content: ""
  display: block
  position: absolute
  left: 50%
  top: 50%
  transform: translate(-50%, -50%)
  transition: 0.5s   // 圈叉變化時間 0.5秒
&:hover  // 與 .circle 同一層級
  &:after, &:before
    +size(90%, 15%)
    background-color: $color_blue
    border-radius: 0px
  &:before
    transform: translate(-50%, -50%) rotate(45deg)
  &:after
    transform: translate(-50%, -50%) rotate(-45deg)
井字圈叉遊戲:步驟二,滑鼠hover移上會轉成叉叉
井字圈叉遊戲:步驟二,滑鼠hover移上會轉成叉叉

3. 調整圈叉的共用CSS屬性

在叉叉測試測試完畢後,由於剛剛 hover 只是用來測試,使用 hover 模擬沒問題後,就將其改為 .cross ,並在 html 新增 .cross 的元素。

//HTML
.block
.block.circle
.block.cross
//CSS
.block
  +size(150px)
  border: solid 1px
  position: relative
  &:after, &:before //change the after and before position
    content: ""
    display: block
    position: absolute
    left: 50%
    top: 50%
    transform: translate(-50%, -50%)
    transition: 0.5s
  &.circle
    &:after, &:before
      border-radius: 50%
    &:before
      +size(90%)
      background-color: $color_red
    &:after
      +size(60%)
      background-color: $color_bg
  &.cross 
    &:after, &:before
      +size(90%, 15%)
      background-color: $color_blue
      border-radius: 0px
    &:before
      transform: translate(-50%, -50%) rotate(45deg)
    &:after
      transform: translate(-50%, -50%) rotate(-45deg)
井字圈叉遊戲:步驟三,調整圈叉的共用CSS屬性
井字圈叉遊戲:步驟三,調整圈叉的共用CSS屬性

4. 進入 Vue.js 世界,綁定建立的樣式

在正式進入 Vue.js 的世界前,我們先模擬使用 jQuery的情況 ,在Console中嘗試新增以及拿掉元素上的 class。

//Console
// 抓取 .block.circle,有 circle 這個 class 就將其拿掉
$(".block.circle").toggleClass("circle")
// 抓取 .block,無 circle 這個 class 就加上去
$(".block").toggleClass("circle")

假如使用 jQuery 來寫的話,首先會先建立一個長度為 9 的陣列 (var blocks = [1,0,-1] ),裡面放置的數字分別代表每一個格的狀態,比如說 1 代表圈圈、-1 代表叉叉、0 代表空,有了這些一串數字後,再根據數字去新增與刪減 Class。

這樣可以達到效果沒錯,但是如果可以當資料更新的時候,外觀也就自動更新,不需要自己再去新增與刪除 Class 的話,會是更好的方法,而我們接下來要使用的 Vue 便可以完成這部分。

在使用 Vue 的起手式上,首先需要一個溝通橋梁,類似搬運工的角色,名為 vm,接著要定義作用的範圍,設定為 el: "#app",而在 HTML 最外面一層上加上 #app。這樣就連接完畢,接下來在 Vue 中所做的資料更新都會做用到整個 HTML 上。

接下來要做的功能是,根據在 Js 中的資料,來判定在 HTML 上的元素要不要加上相對應的 Class。先處理 Vue 的部分,由於所設定的是資料相關,所以要擺放在 data{} 之中,裡面新增一個物件, key 為 blocks,value 為 { type: -1 }

接著來到 HTML,在切換 Class 上,需要 v-bind 這個標籤,它是用來綁定與參數相關的,像是Class、href 就會使用它。後面雙引號部分則是來決定要加上的 Class 以及可否能加的條件 ("{ Class : 成立條件 }"),這裡的 Class 不僅可設定一個,也可以是多個,只需要以逗號相隔開來即可。所以下方 HTML 中所代表的是,如果 type 是 1 的話,那就加上圈圈,而如果 type 是 -1 的話,那就加上叉叉。大家可以利用Console嘗試更新 blocks: { type: -1 } 中 type 數字(輸入 vm.blocks.type=1或0或-1),來觀察外觀上的變化。

//JS
var vm = new Vue({
  el: "#app",
  data: {
    blocks: { type: -1 },
  }
});
//HTML
#app
  .block(v-bind:class="{ circle: blocks.type == 1, cross: blocks.type == -1 }")
  //刪除先前的.block.circle及.block.cross

5. 用 Vue.js 的 Array 產生九個框框

在前一個步驟,完成了最左上角的單一方塊在外觀與資料的連結,但是圈圈叉叉是九宮格,所以老闆使用 Array.from 來產生九個 { type: 1 } 並存放到 Blocks 中,讓 Blocks 是長度為九的陣列。

接著我們來到了 HTML,回想起當需要存取陣列的每一個元素時,就使用迴圈,而迴圈中所設置的變數可依據需求自行定義,而在 Vue 中也是相同的概念,透過 v-for 來存取陣列中的每一個元素,使用方式為 v-for="自訂義名稱 in 陣列" ,由於 blocks 長度是九,所以 Vue 就會在畫面上產生九個.block元素,在這裡老闆使用了 block 來當作識別的變數,所以在判斷 Class 上名稱就是使用 block.type == 1,來判斷每一個 Block 所設置的 type 是不是等於 1。

由於現在設定上都是圈圈 (type: 1 ),老闆後來改以隨機變數的方式產生 -1、0、1 ( type: 1 - parseInt(Math.random() * 3) ),這樣一來畫面上不同方格就會各自呈現不同的形狀。

//JS
var vm = new Vue({ 
  el: "#app",
  data: {
    blocks: Array.from({ length: 9 }, function () {
      return {
        type: 1 
      }
    })
  }
});
//HTML
#app
  .block(v-for="block in blocks",
         v-bind:class="{circle: block.type == 1, cross:block.type == -1}")

v-for與v-bind的差別:v-for用來產生陣列裡的多個複製物,並存取或綁定陣列中的各個元素,v-bind:屬性可將符合條件的單個元素綁定屬性

井字圈叉遊戲:步驟五,用 Vue.js 的 Array 產生九個框框
井字圈叉遊戲:步驟五,用 Vue.js 的 Array 產生九個框框

6. 調整HTML及CSS排列成為九宮格

在步驟五,透過 v-for 自動產生九個方塊了,在方塊中也隨機擺放著不同的符號。不過在圈圈叉叉的設置上是水平三格與垂直三格所組成的九宮格,因此要在這組 .block上再新增一個母元素取名為 .block_area,用來規範整個九宮格的大小與位置。我們首先在每個框框中加入編號:

//HTML
#app
  .block_area
    .block(v-for="(block,bid) in blocks",
           v-bind:class="{circle: block.type == 1, cross:block.type == -1}")
      .small_number {{ bid+1 }}

在 css 的設定上,由於裡面的框框預設上的寬高都是 150px,所以在外框的寬度上就乘上 3 倍。在排列方式上使用 flex,在預設上是當母元素也就是 .block_area 加上 flex 後,下層的子元素 .block 就會依序由左向右排列且不換行。為了達成換行的效果,會需要加上 flex-wrap: wrap ,這樣當寬度超過時,子元素就會自己自動往下排列了。

//CSS
.block_area
  +size(150px*3)
  display: flex
  flex-wrap: wrap
井字圈叉遊戲:步驟六,調整HTML及CSS排列成為九宮格
井字圈叉遊戲:步驟六,調整HTML及CSS排列成為九宮格

結果此時會發現還是不太對,確實有往下排列了沒錯,但是卻以兩格兩格的方式排列。原因在於,在預設上每一個元素的寬度 = Set width + border + padding,以現在的框框來說就會是 150px +1px*2 = 152px,所以當排列到第三個框框時就會超出範圍,自然地就會向下排來排列。為了要讓指定的寬度 150px 涵蓋 border 的寬度,那就要再加上 box-sizing: border-box 就可以囉。

//CSS
.block
  +size(150px)
  border: solid 1px rgba(white, 0.2)
  position: relative
  box-sizing: border-box
井字圈叉遊戲:步驟六ㄓ之之ㄧ之一之一,調整HTML及CSS排列成為九宮格

7. Vue.js中設定重新開始

要如何設定所有物件重置呢?回想一下在第五步驟中,提到框框中的圖形是根據 type 的數字來做變化的,所以要將畫面清空,僅需要將數值統一設定為零即可。然而將清空畫面屬於動作,是需要視情況不斷執行的,所以要將重置的設定寫在 methods 中。

設定好後,會發現畫面上的方格都消失了,原因在於一開始 Vue 的元件建立後,並不會自動執行寫在 methods 中的函式,為了讓元件建立後就初始化將每個方格方格設定為空,必須在 mounted() 也就是 Vue 初始化剛完成時,執行 restart() 函式。

//JS
var vm = new Vue({
  el: "#app",
  data: {
    blocks: [],
  },
  mounted() {
    this.restart()
  },
  methods: {
    restart() {
      this.blocks = Array.from({ length: 9 }, function () {
        return {
          type: 0
        }
      })
    }
  }
});

8. 點擊後下棋

設定好九宮格初始化的格式後,就要來使用滑鼠點擊。前面有提到,在 Vue.js 中與樣式有關的會使用 v-bind,而點擊是動作,這與事件有關則是使用 v-on。所以要達成當點擊框框內後會呈現圈圈的語法如下 v-on:click="block.type=1",表示當偵測到點擊時,就把 type 設定為 1 ,也就是圈圈的形式。此時會發現不管點擊畫面上哪一位置都會變成圈圈。

//HTML
#app
  .block_area
    .block(v-for="(block,bid) in blocks",
           v-bind:class="{circle: block.type == 1, cross:block.type == -1}"
           v-on:click="block.type=1")
    .small_number {{ bid+1 }}

確定點擊效果沒問題後,接著來製作輪流下棋。輪流就是將 type 一開始設定為 1,再來就是 -1,並接續輪流交替,由於數值的切換是動作,所以透過將 v-on:click 後面包成一個動作 player_go(block),讓設定的細節到 Vue 中函式做處理。每當有方塊偵測到點擊時,就會傳送當下被點擊方塊設定 type,並且會交替的更換 turn 的數值,讓 turn 在 1 與 -1 兩者之間交替。可以注意到,在 data 中新增了資料 turn: 1,代表遊戲開始是由圈圈這方開始,若遊戲要從叉叉開始,那只要改為 turn: -1 就可以囉。

//HTML
#app
  .block_area
    .block(v-for="(block,bid) in blocks",
           v-bind:class="{circle: block.type == 1, cross:block.type == -1}"
           v-on:click="player_go(block)")
      .small_number {{ bid+1 }}
//JS
var vm = new Vue({
  el: "#app",
  data: {
    blocks: [],
    turn: 1
  },
  mounted() {...
  },
  methods: {
    restart() {...
    },
    player_go(block) {
      block.type = this.turn
      this.turn = -this.turn
    }
  }
});

9. 顯示下棋者

在與九宮格同一的層級之下,新增一個 .block.small 的元素,另外會在綁定 vue 中的資料 turn,這樣一來加上相對應的 Class。另外,由於這只是作為提示框而非遊戲操作部分,所以將格子縮小會比較好看一些。

//HTML
#app
  .block_area
    ...
  .block.small(v-bind:class="{circle: turn == 1, cross: turn  == -1}")
//CSS
.block
  +size(150px)
  border: solid 1px rgba(white, 0.2)
  position: relative
  box-sizing: border-box
  &.small //縮小格子
    +size(60px)
井字圈叉遊戲:步驟九,顯示下棋者
井字圈叉遊戲:步驟九,顯示下棋者

10. 理出邏輯,再用Vue.js判斷贏家

本小節是此範例中最核心也是最有挑戰的的地方:要如何判斷有贏家產生,而贏家又是哪一方呢?以下先用圖示講解觀念,再著手進入程式。

圈圈叉叉井字遊戲判斷贏家

在畫面上,每一格都有相對應的數值,圈圈是 1,叉叉是 -1,而都沒有劃記則是 0,那可以得知只要有某一條線的所有數值相加起來為 3 或是 -3 ,這就代表有連成一線。接著要考慮可連成一線的方式有哪些情況,下面以格子的數字做為表示,一共有八種情況,分別是 123 / 456 / 789 / 147 / 258 / 369 / 159 / 357,每當有玩家點選方格時,便會依序檢查上述八種情況的任一是否有連線成功。

接著在進入撰寫連線的程式前,先來認識在 Vue 中新的小夥伴 – computed,它會自動監看在 Vue 中的數值是否有更動了。當有內部有數值更新時,所設定的屬性也會隨之更新。下面以 user() 作為範例,當數值 turn 改變時, user() 所計算過後回傳的值也會改變,而這個值可放在 HTML 中來使用。

//JS
var vm = new Vue({
  ...
  methods: {
    restart(){...
    },
    player_go(block) {...
    },
    computed:{
      user(){
        return this.turn == 1? "O'turn" : "X'turn"
      }
    }
  }
})
//HTML
#app
  h1 {{ user }}
  ...
井字圈叉遊戲:步驟十,顯示出輪到誰下棋
井字圈叉遊戲:步驟十,顯示出輪到誰下棋

接下來就要開來寫連線判定的部分囉,先將前面所提到的連線情況存成一組字串,接著透過字串分割的方式形成陣列,並回傳數值顯示在畫面上:

//JS
computed:{
  pattern_data(){
    var verify_list = "123,456,789,147,258,369,159,357"
    var result = verify_list.split(",")
    return result
  }
}
//HTML
#app
  h1 {{ pattern_data }}
  ...
井字圈叉遊戲:步驟十,寫贏家判斷列
井字圈叉遊戲:步驟十,寫贏家判斷列

為了可以顯示出每一個格子的序號與相對狀態,所以在原本顯示狀態的 type 之上加上屬性 id,由於程式是由 0 開始,格子的序號是由 1 開始,所以 id = i +1。

//JS
restart(){
  this.blocks = Array.from({ length: 9 }, function (d,i) {
    return {
      id: i+1, // 新增序號 
      type: 0
    }
  })
},

這樣子九宮格所呈現的資料會是如下,總共九個物件,每一個物件都會有相對應 id 以及 type

[ { "id": 1, "type": 0 }, { "id": 2, "type": 0 }, { "id": 3, "type": 0 }, { "id": 4, "type": 0 }, { "id": 5, "type": 0 }, { "id": 6, "type": 0 }, { "id": 7, "type": 0 }, { "id": 8, "type": 0 }, { "id": 9, "type": 0 } ]

回到 pattern_data(),依據剛剛所列出的驗證組合,取出相對應的物件,比如說:

[ { "id": 1, "type": 0 }, { "id": 2, "type": 0 }, { "id": 3, "type": 0 }]

在陣列的轉換上使用 map,以 vtext 來代表每一項驗證的組合。由於是要將長度九的陣列依照條件轉換成各個長度為三的陣列,透過使用 filter 來進行來過濾,過濾的條件則是察看序號是否相同,這裡使用到 indexOf,若有相同數值會回傳 1,否則則會回傳 -1 。

取出陣列組合後,可以點擊看看九宮格,確認 type 的是數值是會有變化的,而這個數值就是用來相加並進行判斷的,再次使用 map 來取出陣列中 type 數值的部分,並且透過 reduce 來計算三個數值相加總合。

//JS
var verify_list = "123,456,789,147,258,369,159,357"
var result = verify_list.split(",")
  .map((vtext)=>{
    var add = this.blocks
      .filter((d, i) => vtext.indexOf(i + 1) != -1)
      .map((d) => d.type)
      .reduce((a, b) => (a + b), 0);
    return add;
  })
return result

下方圖片標示出在做完每一項操作後,所產生出的結果。

井字圈叉遊戲:步驟十,.filter() 過濾出驗證的陣列
井字圈叉遊戲:步驟十,.filter() 過濾出驗證的陣列
井字圈叉遊戲:步驟十,.map() 數值 tpye
井字圈叉遊戲:步驟十,.map() 數值 tpye
井字圈叉遊戲:步驟十,.reduce() 相加數值
井字圈叉遊戲:步驟十,.reduce() 相加數值

目前有個每一個驗證規則的數值加總,但是只需要回傳贏家的判斷即可,也就是 3 (三個圈成一線)與 -3 (三個叉成一線),所以使用到 .filter() ,判斷條件為只要絕對值等於 3 就代表有贏家產生了。除了知道有贏家外,也希望可以知道是哪一條線成立的,所以在回傳值的上面也加上了 rule: vtext

//JS
var verify_list = "123,456,789,147,258,369,159,357"
var result = verify_list.split(",")
  .map((vtext)=>{
    var add = this.blocks
      .filter((d, i) => vtext.indexOf(i + 1) != -1)
      .map((d) => d.type)
      .reduce((a, b) => (a + b), 0);
    return { rule: vtext, value: add }
  })
result = result.filter((obj) => Math.abs(obj.value) == 3)
return result
井字圈叉遊戲:步驟十,贏家條件成立時,顯示驗證結果

11. 在畫面上顯示贏家

有了贏家之後,需要顯示在畫面中,我們在 computed:{} 新增另一個 win_text() 的函式。在預設上 winner = -1,代表沒有任何贏家產生,而當 this.pattern_data.length > 0,則表示有贏家產生數值時,將 winner 的值替換,再依據是正 3 還是負 3 顯示贏家。

//HTML
#app
  h1 {{ win_text }}  // win text
  .block_area
  .block(v-for="(block,bid) in blocks",
         v-bind:class="{circle: block.type == 1, cross:block.type == -1}"
         v-on:click="player_go(block)")
    .small_number {{ bid+1 }}
  .block.small(v-bind:class="{circle: turn == 1, cross: turn == -1}")
//JS
win_text() {
  var winner = -1
  if (this.pattern_data.length > 0) {
    winner = this.pattern_data[0].value
  }

  if (winner == 3) {
    return 'O wins'
  } else if (winner == -3) {
    return 'X wins'
  }
  return (this.turn == 1 ? 'X' : 'O') + "' turn"
}

12. Vue.js中加上「防止每格內的重複點擊」的機制

在贏家產生後,照理來說遊戲就該停止了,但是現在遊戲還是可以不斷進行,原因在於還沒有防止重複點選機制。解決方式很簡單,只需要在點選前加上判斷格子是否為空的 block.type == 0 就可以了。

//JS
player_go(block) {
  if (block.type == 0) {
    block.type = this.turn;
    this.turn = -this.turn;
  } else {
    alert("Not allow")
  }
}

13. 增加按鈕重新開始遊戲

在遊戲結束後,需要清空畫面才能將再次進行遊戲,所以在最下方加上重新開始遊戲的按鈕。前面有提到 Click 動作點擊要搭配的前綴字為 v-on:click ,這可以縮寫成 @click,而清空的功能在步驟七的地方已經寫好了,所以這裡只需要呼叫就可以囉。

//HTML
#app
  h1 {{ win_text }}
  .block_area
    .block(v-for="(block,bid) in blocks",
           v-bind:class="{circle: block.type == 1, cross:block.type == -1}"
           v-on:click="player_go(block)") 
      .small_number {{ bid+1 }}
  .block.small(v-bind:class="{circle: turn == 1, cross: turn  == -1}")
   h2(@click="restart") Restart   // Restart 重新遊戲

14. 最後來CSS裡調整畫面

目前元素都靠向左側,我們希望調整到整個畫面的中心位置對齊,會比較好看。除了 htmlbody 要將子元素#app 排列置中, #app 裡的所有元素也必須排列整齊垂直置中,所以在元素設定上也一同加上了 #app

在排列上使用 flex,前面有提到,flex 預設上是橫列的由左向右排列,但是我們要的是由上而下的垂直排列,所以方向設定上改成 flex-direction: column,在主軸 justify-content 以及 交叉軸 align-items 都設定為置中 center。

//CSS
html, body, #app
  background-color: $color_bg
  margin: 0
  color: white
  +size(100%)
  display: flex
  justify-content: center
  align-items: center
  flex-direction: column
//HTML
h2 Player  // 在顯示輪到誰的圖示前面加上文字 Player
.block.small(v-bind:class="{circle: turn == 1, cross: turn  == -1}")
最後調整CSS,完成遊戲

結語

我們總結一下這次的圈圈叉叉遊戲的範例,製作過程可以分為五個部分:

  1. 以CSS擬元素(:before, :after)的方式繪製圈圈叉叉。
  2. 透過 Vue 的 Array 產生九個框框,並且透過綁定資料,讓畫面可以根據資料顯示對應的樣式。
  3. Restart – 重新遊戲,可在遊戲一開始或是遊戲中點擊 Restart 後清空畫面。
  4. 利用 v-on:click 選寫下棋功能。
  5. computed:{}計算出贏家成立的條件以及在畫面上顯示贏家。

這就是我們用 CSS及Vue.js 寫出來的圈圈叉叉遊戲啦!老闆的成品這邊去,也非常歡迎大家到社團裡跟我們分享你們完成的作品。

動畫互動網頁程式入門(HTML/CSS/JS)以簡單例子帶你入門網站的基礎架構及開發,用素材刻出簡單有趣又美觀的網頁和動畫,享受做出獨一無二的網頁所帶來的成就感,在職場上與設計師和工程師合作無間。

打好基本的互動網頁基礎之後,可以進階動畫互動網頁特效入門(JS/CANVAS),紮實掌握JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。長達3085分鐘,超過60個精緻範例與400張的投影片以上,以及四個加碼單元vue-cli、GSAP、D3、Three.js的投影片,成為hahow上最長的課程。

此篇直播筆記由幫手 阮柏燁 協助整理

墨雨設計banner

這篇文章 Vue.js入門:完成懷舊的井字圈叉遊戲動態網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>