這是一個部落格,文章都寫在issue裡面,內容包含程式與設計。 This a issue blog of Programing and Design knowledge.
以下是寫在hackmd的版本,issue那邊也有紀錄。
這是一個部落格,文章都寫在issue裡面,內容包含程式與設計。(This a issue blog of Programing and Design knowledge)
這是一個部落格,文章都寫在issue裡面,內容包含程式與設計。 This a issue blog of Programing and Design knowledge.
以下是寫在hackmd的版本,issue那邊也有紀錄。
從網路影片中整理出的魔王設計心得,很多元素不特定是用在Boss上,也適用在一般敵人。
Telegraphing在Google翻譯上是電報的意思,Gamasutra網站裡面的文章Enemy Attacks and Telegraphing有不錯的解釋,但我還是沒找到合適的中文翻譯,我自己先翻成"預備動作"好了。
Telegraph (v) - To convey a message, intentionally or not, especially with facial expression or body language.中文意思是有意無意的釋出訊息,特別是肢體語言或是臉部語言
Telegraphing在這邊指的是Boss在出招前的預備動作,讓玩家知道要發生什麼事了,有時間做出反應。
Telegraphing可以明顯或不明顯,但不能完全讓人摸不著頭緒,比如說Boss要吐火前會有個吸氣的動作,讓玩家可以有時間反應,不然玩家會玩到腦羞。
有些攻擊力越強大的攻擊,Telegraphing的時間會越長,像是魔王在射出超大雷射光束前的集氣時間通常會比較久。
雖然魔王的攻擊有固定的行為模式,但如果每次出招的方式都一模一樣,會太容易被摸透,這樣玩起來會很無聊,所以在裡面加上隨機的要素很重要。
比如說火龍要吐火球的動作可以讓玩家察覺,但是會吐出一顆火球還是兩顆就不一定,或是火球是往左上方發射還左下發射也是未知的,這樣的隨機考驗玩家瞬間反應能力,所以玩家必須要專注去觀察火球的隨機方向與隨機速度,遊戲才會更有挑戰。
這其實很常見,魔王在不同的階段會有不同的攻擊模式,如果你的魔王只有一個階段,你又想延長魔王戰的時間,有些人會把魔王血量調高,但那並非是好的設計,透過讓Boss有不同階段,可以讓延長魔王戰的時間,而且不失趣味。
jerryklu:
要研究BOSS戰的設計我會推薦直接先從MGS全系列的BOSS戰看起,MGS真正唯一無人能及的就是BOSS戰的變化,你這些整理我猜基本上都是從日系風格遊戲來的,可以理解,但是在美系或是像MGS這種日美系混和風的有時候,例外就會有,雖然大原則還是差不多,但我想更理想的設計方式應該要強調你裡面寫到"考驗玩家技能"這個方向開始,因為隨機跟預備動作其實小兵也會有,只是程度的問題,還有另一個原因就是甚至要考量到需不需要有BOSS戰的問題,例如走寫實路線的FPS如COD或BF就不適合,另外一種是像起源以前的刺客教條,其實也不能算有BOSS戰,一個是他核心的機制不夠完備,不管是拳腳戰還是匿蹤,都不夠做出變化,另外一個是純潛行的BOSS戰本來就更難做,MGS大概有一半的BOSS戰跟潛行沒關係,甚至有些BOSS戰是幾乎全新的動作設定,所以簡單來說整個BOSS戰的根還是要先從你遊戲機制的核心多元度,再考慮整體風格,然後BOSS戰才有戲
LayerZ:
寫實路線FPS,可以直接看全境封鎖XD 可是後來就沒有什麼真的難的BOSS,FPS的難易度太難抓了,難起來一直被秒,全境後來是靠全域事件撐起來,各種秒本來很難過的王,那是後話,COD之前的BOSS戰與其說難易度,都是作氣氛的,但是這一代
BO4對於槍戰的打擊感總算抓到巧妙的平衡,不會過於秒殺,也不會打了沒感覺的FU,然後回你內文,你必須要把隨機性跟可預期性兩件事分開
ImCasual:
這說是Boss 不如說是會攻擊的敵人吧!我覺得Boss最重要的是一個「總結」感,讓人可以從遊戲的經驗當中找出這個Boss的打法,就會感覺到「我玩這個遊戲就是為了這刻」,動作遊戲就是 用你摸熟了的機制應對攻擊,劇情遊戲 重點會在讓你想起自己歷經過的事件,小Boss中Boss採用單純變化性大的沒問題 很有趣,不過最終Boss 必須把玩家投入的時間和情緒列入考慮,Undertale和平結局Boss戰超級感人,各種爆哭,但是那個戰鬥如果沒把遊戲打完就是個三洨,直接把玩家丟到那個結局 那個就不會有用,呃 總之我要說的是 Boss戰的設計 定義超廣,建議把標題限縮一下範圍
大家好,有一些 Naming Convention 的議題想跟各位分享與討論
勒布朗克法則(LeBlanc’s Law)説:Later equals Never
你心想著待會兒再回來整理程式,但其實永遠不會再回頭改的
最近想要整理出一些guideline跟同仁分享,想說這樣大家的程式可讀性提升,對團隊的合作會更好,不過我想先拋磚引玉,分享一些我整理出來的心得,希望大大們可以分享一下自己的 Naming Convention 或是有更好或更多的撰寫程式的規範跟原則。
參考書籍:
下方先是我整理的資料分享,歡迎大家也分享自己的 Naming Convention
Boolean變數或function的開頭必須使用is, can, has, should,但如果本身是形容就不需要,像是enabled, done就無需用is開頭
使用肯定的Boolean變數名稱。避免雙重否定:if (!notFound) { }。
這件事情但起來單純,但很多時候大家都會忽視介系詞硬組變數名稱,像是daysUntilDeadline就有可能被命名成DeadlineDays,如易造成誤解,以下是我找到一些不錯的範例
var daysSinceModification = 3; // 修改後已過了3天
var workDaysPerWeek = 5; // 每週工作5天
var daysUntilDeadline = 10; // deadline前還剩10天
var customersInOrder; // 已經排序過的客戶資料
delayedTime代表的是delay的時間,但其實單位是秒數,所以命名上最好使用更具體的單位來優化這個命名,改寫成delayedSeconds會更好
如果是一筆員工的資料,原先變數取名是person,那最好將person重新命名為employee。person的含義太寬泛,employee則能表示員工的概念。
一般for迴圈中我們都會使用到i, j, k,這其實不是不行,但是如果i, j, k分別代表parent, children, toy,那就可以用pi, ci, ti來取代i, j, k,這樣程式碼不會變長太多,可讀性提高也較容易debug。
不過更好的方式就是直接使用array.forEach()
或array.map()
,會比iterator更清楚明瞭。
基本上function名稱全都是「動詞」開頭但如果像是getCellSize()
或calcCellSize()
,可以直接省略開頭的動詞,直接使用cellSize()
即可,不過也有人堅持不能用名詞開頭,基本上團隊統一即可。
因為不想打太長的變數或函數名稱,大家總是在縮寫,但是有些縮寫很容易讓人搞混,對於原本就在維護這份程式的工程師或許還好,但新進工程師會很容易困惑。
使用像是dns,url這些眾所皆知的詞沒問題,只有自己團隊才看得懂的縮寫,除非必要,不然盡量避免使用。
不過我覺得這個看情況而定,重點是一定要有一份解釋縮寫的文件比較好。
TODO
FIXME
等註解標籤,TODO
可以標記尚未製作或是需要優化的部分,FIXME是不能運作需要修復的部分,其實我知道還有HACK
, XXX
等標籤,不過我覺得好像都歸類在TODO比較方便,不知道大家的習慣是如何?// 加上合理的限制 - 沒有人能讀那麼多文章
const int MAX_RSS_SUBSCRIPTIONS = 1000;
如果if()裡面有超長一串的判斷式,可讀性會超差,所以可以把這個判斷式用boolean變數裝起來再判斷
boolean isMan = (user.age >= 18 && user.gender == 'male');
if (isMan){
// 做一位男人該做的事>///<
}
Clean Code這本書上有說Pick one word for one abstract concept and stick with it.
不過比較沒人整理出較為通用的詞庫對應表,我知道這個本來就沒官方解答,但是我想聽聽看各位的經驗,整理出我自己的一套詞庫,以下是我目前整裡的詞庫
剩下還有哪些詞是大家常用的呢?還請各位大大不吝分享,我在下面放一些可能會用到的詞,大家是怎麼給這些詞定義他專屬的concept,或是有哪些沒提到的也可以補充一下,我已經看過微軟 PowerShell 指令動詞列表了,不過想說如果大家有更具體的應用,也可以分享一下,麻煩大家給點意見了
get/set
create: insert append add append
edit: modify update
complete: finish done end
send: deliver, dispatch, announce, distribute, route
find: search, extract, locate, recover
start: launch, create, begin, open
make: create, set, build, genernate, compose, add, new
參考資料:小酌重構系列[18]—重命名
這份文件會告訴你如何在VS Code中使用Macros,首先你要先安裝Macros套件,然後設定2~3個json檔即能使用Macros。
settings.json
{
"macros": {
"makeAction": [
"editor.action.addSelectionToNextFindMatch",
"editor.action.clipboardCopyAction",
"cursorEnd",
{
"command": "type",
"args": {
"text": ":wrapper.instance()."
}
},
"editor.action.clipboardPasteAction",
{
"command": "type",
"args": {
"text": ","
}
}
],
"makeFunctionTest": [
"editor.action.addSelectionToNextFindMatch",
{
"command": "editor.action.insertSnippet",
"when": "editorTextFocus",
"args": {
"name": "functionName2testCase"
}
}
]
}
}
安裝完macros套件後,就能在setting.json
中加入macros這個屬性,這次實作了makeAction與makeFunctionTest兩個macros command。
makeAction功用:將任意一個單字word
變成word: wrapper.instance().word,
editor.action.addSelectionToNextFindMatch
:選取離游標最近的一個單字,預設快捷鍵是command+Deditor.action.clipboardCopyAction
:複製cursorEnd
:將游標移至行末,也就是最右邊type
:輸入文字,輸入的內容填在args
中的text
editor.action.clipboardPasteAction
:貼上,也就是貼上剛剛複製的內容type
補上一個逗號makeFunctionTest功用:將任意一個單字word
變成下方程式碼
it('word()', () => {
const { actions } = setup();
// if you want to mock word(), then use actions.mock('word');
//actions.word();
// TODO: check the result correct or not
});
editor.action.addSelectionToNextFindMatch
:選取離游標最近的一個單字,預設快捷鍵是command+Deditor.action.insertSnippet
:插入程式碼片段(Snippet),在本次範例中我使用自己撰寫的snippetfunctionName2testCase
,請參考下方的javascript.json
keybindings.json
[
{
"key": "ctrl+a",
"command": "macros.makeAction"
},
{
"key": "ctrl+s",
"command": "macros.makeFunctionTest",
}
]
把剛剛在setting.json
中設定好的macros command配上對應的快捷鍵。
javascript.json
{
"functionName2testCase": {
"prefix": "f2t",
"body": [
"it('$TM_SELECTED_TEXT($0)', () => {",
" const { actions } = setup();",
" // if you want to mock $TM_SELECTED_TEXT($0), then use actions.mock('$TM_SELECTED_TEXT');",
" // actions.$TM_SELECTED_TEXT($0);",
" // TODO: check the result correct or not",
"});"
],
"description": "description of functionName2testCase"
}
}
如果還沒使用過自訂snippet功能的可以看這邊。
剛剛使用editor.action.insertSnippet
要插入的snippet如上,body就是實際會被插入的程式碼,其中要注意的是$TM_SELECTED_TEXT
與$0
。
$TM_SELECTED_TEXT
:這是一個保留字,代表正被游標選取的文字,假如說我剛剛選取了一段程式片段word
,接著插入snippet,此時的$TM_SELECTED_TEXT
就是word
$0
:在插入sinppet後,游標會停留在$0
的位置,如果有多個$0
,就會有多個游標,可以讓你輸入客製化的內容,除了$0
之外還有$1
,$2
,$3
以此類推,輸入完$0
後就會跳到$1
讓你繼續輸入。
為了讓console.log()更加方便使用,我製作了speedConsoleLog,將任意一個單字word
變成下方程式碼
console.log('word');
console.log(word);
settings.json
{
"macros":
"speedConsoleLog": [
"editor.action.copyLinesUpAction",
"cursorHome",
{
"command": "type",
"args": {
"text": "console.log('"
}
},
"cursorEnd",
{
"command": "type",
"args": {
"text": "');"
}
},
"cursorDown",
"cursorHome",
{
"command": "type",
"args": {
"text": "console.log("
}
},
"cursorEnd",
{
"command": "type",
"args": {
"text": ");"
}
},
]
}
文件大綱:
直接先看一下JSON schema的範例,暖個身
{
"title":"產品資訊",
"description":"產品包含商品標號、商品名稱及價錢",
"type":"object",
"properties":{
"pid":{
"description":"產品的id,是product id的縮寫",
"type":"integer"
},
"name":{
"description":"產品名稱",
"type":"string",
"maxLength": 10
},
"price":{
"type":"number",
"exclusiveMinimum":0
}
},
"required":[
"pid",
"name",
"price"
]
}
JSON Schema就是JSON資料結構的規範,規定一個JSON資料該有哪些資料,包含規定型態、大小範圍等。JSON Schema有官方規範,可以在http://json-schema.org上查看,目前已出到draft-07。
這份文件要介紹的 Ajv (Another JSON Schema Validator) 套件會支援 draft-04/06/07 這幾個版本。
JSON Schema 功用如下:
網路上已經有文件把9成的功能介紹完了,廢話不多說直接看JSON Schema 辭典吧!
其他教學文件:
比起看文件,大家更愛範例,所以在這邊提供幾個範例,剩下的可以去上面的文件看。
資料是數字(在這邊成為n), 3 <= n < 9 且 n要是3的倍數
{
"type": "number",
"minimum": 3,
"exclusiveMaximum": 9,
"multipleOf": 3
}
exclusiveMinimum
與exclusiveMaximum
在draft-04以前是boolean,但在draft-06以後則變成數字。字串長度為5~10
{
"type": "string",
"minLength": 5,
"maxLength": 10,
}
符合正規表示式的字串,輸入的字串必須符合身分證字號的格式
{
"title": "ID card number",
"type": "string",
"pattern": "^[A-Z]{1}[0-9]{9}$"
}
符合ipv4的的字串,還有email
跟date-time
等format
{
"type": "string",
"format":"ipv4"
}
陣列長度2~5,而且必須都是數字,並不得有重複的數字
{
"type": "array",
"items": {
"type": "number"
},
"minItems": 2,
"maxItems": 5,
"uniqueItems": true
}
陣列內只能有指定的[ 整數
, DC或Marvel
],不可以有其他東西
{
"type": "array",
"items": [
{
"description": "粉絲的年紀",
"type": "integer"
},
{
"type": "string",
"enum": ["DC", "Marvel"]
}
],
"additionalItems": false
}
整數
, DC或Marvel
, 這邊要放多少東西都可以
],additionalItems可以指定type等相關資訊,代表多出來的陣列內容必須符合additionalItems的限制。Object物件中,最多只能有5個property,一定要有nickname
,當有carMileage
時,就一定要有carBrand
,反之則不需要,carBrand
只能填bmw或benz,除了原本定義的property之外,其他的property都得是string型態。
{
"type":"object",
"maxProperties":5,
"properties":{
"nickname":{ "type": "string" },
"carBrand":{ "enum": ["benz", "bmw"] },
"carMileage":{ "type":"number" }
},
"required": ["nickname"],
"dependencies":{ "carMileage": ["carBrand"] },
"additionalProperties":{ "type":"string" }
}
oneOf
代表 只能 符合其中一項,也就是這個數字必須是5或3的倍數,但不能是15的倍數,另外還有anyOf
跟allOf
以及not
可以使用
{
"type": "number",
"oneOf": [
{ "multipleOf": 5 },
{ "multipleOf": 3 }
]
}
使用refs引用定義好的definitions,先定義好正整數後直接引用
{
"type": "array",
"items": { "$ref": "#/definitions/positiveInteger" },
"definitions": {
"positiveInteger": {
"type": "integer",
"exclusiveMinimum": 0
}
}
}
definitions相當於引用自己建立的變數的感覺,厲害的是$refs可以引用外部檔案的schema,像是json-schema.org提供的官方範例geo.json
{
"$ref": "http://json-schema.org/geo.json#"
}
{ "contains": { "type": "integer" } }
means that any array with at least one integer, any non-array is matchedif-else的範例
{
"type": "integer",
"minimum": 1,
"maximum": 1000,
"if": { "minimum": 100 },
"then": { "multipleOf": 100 },
"else": {
"if": { "minimum": 10 },
"then": { "multipleOf": 10 }
}
}
符合規定的資料: 1, 5, 10, 20, 50, 100, 200, 500, 1000
不符合規定的資料:
The "$schema" keyword
http://json-schema.org/schema#
,目前如果你打開來看的話會是draft-07,如果想指定成draft-06版的話可以寫成http://json-schema.org/draft-06/schema#
The "$id" keyword - schemaId
"http://json-schema.org/geo"
,別人使用$ref想遠端過來取用這個schema就是對應這個$id,object中的property也可以有$id,有如path的概念#/definitions/B
,取用外部的話則用http://example.com/other.json
,詳細教學看這邊 {
"$id": "http://example.com/root.json",
"definitions": {
"A": { "$id": "#foo" },
"B": {
"$id": "other.json",
"definitions": {
"X": { "$id": "#bar" },
"Y": { "$id": "t/inner.json" }
}
},
"C": {
"$id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f"
}
}
}
JSON Schema Validator就是專門來驗證JSON的工具,將要比對的資料拿去跟schema比對,馬上就知道資料符不符合規定,並且能夠得知是哪個資料的哪個環節不符合規定,相當方便,目前星星數最多,而且還有在維護的就屬Ajv(Another JSON Schema Validator)莫屬了,以前我用過的JSON Schema Validator是tv4。
另外有一個react-jsonschema-form,只要在<form>
物件中放入schema跟data,它就能自動生成Bootstrap表單,它的介紹語是這樣寫的"A simple React component capable of building HTML forms out of a JSON schema and using Bootstrap semantics by default."
react-jsonschema-form 程式範例
import React, { Component } from "react";
import { render } from "react-dom";
import Form from "react-jsonschema-form";
const schema = {
title: "Todo",
type: "object",
required: ["title"],
properties: {
title: {type: "string", title: "Title", default: "A new task"},
done: {type: "boolean", title: "Done?", default: false}
}
};
const log = (type) => console.log.bind(console, type);
render((
<Form schema={schema}
onChange={log("changed")}
onSubmit={log("submitted")}
onError={log("errors")} />
), document.getElementById("app"));
但因為綁定Bootstrap有一點死,所以這邊還是選用比較有彈性的ajv。
注意:完整的文件可以直接去github上看,這邊會提到比較常用到的內容,或是我有用過的內容。[npm ajv 連結]
這邊有模擬在Nodejs上面執行ajv的連結,可以進去試用看看。
我們已經學會了JSON Schema了,現在直接使用Ajv來比對schema跟data就能驗證資料了,看看下面的範例吧!符合條件的話ajv.validate就會回傳true,反之則false
var ajv = Ajv();
var schema = {
"type": "number",
"oneOf": [
{ "multipleOf": 5 },
{ "multipleOf": 3 }
]
};
var data10 = 10;
var data15 = 15;
console.log(ajv.validate(schema, data10)); // true
console.log(ajv.validate(schema, data15)); // false
如果想知道錯在哪裡,我們就輸出ajv.errors
,也是直接看下面範例。
我們在字串長度應該是5的schema中輸入長度是4的字串
var ajv = Ajv();
var schema = {
"type": "string",
"minLength": 5
};
var data = "four";
var valid = ajv.validate(schema, data);
if (!valid) console.log(ajv.errors);
資料長度不符合規定時Ajv回傳的錯誤訊息格式如下,大致上看得懂,我們去看下個範例後再解說這些參數是幹嘛的。
[
{
"keyword":"minLength",
"dataPath":"",
"schemaPath":"#/minLength",
"params":{
"limit":5
},
"message":"should NOT be shorter than 5 characters"
}
]
這次的範例有兩個錯誤,分別是2016-12-99
不符合date
格式,以及fruit
欄位應該要是字串卻填入了數字,如果要顯示超過一個以上的error就必須在Options欄位加上{ allErrors: true }
,否則它只會回傳第一個error。
var ajv = new Ajv({ allErrors: true });
var schema = {
"type": "object",
"properties": {
"purchaseDate": { "format": "date" },
"foods": {
"type": "object",
"properties": {
"fruit": { "type": "string" }
},
},
}
};
var data = {
"purchaseDate": "2016-12-99",
"foods": {
"fruit": 5566
}
};
var valid = ajv.validate(schema, data);
if (!valid) console.log(ajv.errors);
回傳了兩個錯誤
[
{
"keyword":"format",
"dataPath":".purchaseDate",
"schemaPath":"#/properties/purchaseDate/format",
"params":{
"format":"date"
},
"message":"should match format 'date'"
},
{
"keyword":"type",
"dataPath":".foods.fruit",
"schemaPath":"#/properties/foods/properties/fruit/type",
"params":{
"type":"string"
},
"message":"should be string"
}
]
想要知道error object的架構,可以看Validation errors,我把裡面的部分內容節錄在下方。
keyword
: validation keyword.dataPath
: the path to the part of the data that was validated. By default dataPath uses JavaScript property access notation (e.g., ".prop[1].subProp"). When the option jsonPointers is true (see Options) dataPath will be set using JSON pointer standard (e.g., "/prop/1/subProp").schemaPath
: the path (JSON-pointer as a URI fragment) to the schema of the keyword that failed validation.params
: the object with the additional information about error that can be used to create custom error messages. [params文件]message
: the standard error message (can be excluded with option messages set to false).API列表 - 接在ajv後面的api都寫在這。
.compile(Object schema)
validating function and cache the compiled schema for future use,比起每次都用ajv.validate()
的執行速度還快,官方文件也說這是The fastest validation call
.errorsText([Array<Object> errors [, Object options]])
ajv.errorsText(ajv.errors)
這樣的寫法可以把error object轉換成一個錯誤訊息字串,
remote schemas have to be added with addSchema or compiled to be available
.addFormat(String name, String|RegExp|Function|Object format)
使用addFormat
定義客製化的format,可以參考下面的範例
var ajv = new Ajv().addFormat('cellphone', '^09[0-9]{2}-[0-9]{3}-[0-9]{3}$');
var schema = {
"format": "cellphone"
};
var data = '0912-345-678'; // format have to be 09XX-XXX-XXX
var validate = ajv.compile(schema);
console.log(validate(data)); // true
.addKeyword(String keyword, Object definition)
自訂keyword
ajv.addKeyword('range', {
type: 'number',
compile: function (sch, parentSchema) {
var min = sch[0];
var max = sch[1];
return parentSchema.exclusiveRange === true
? function (data) { return data > min && data < max; }
: function (data) { return data >= min && data <= max; }
}
});
var schema = { "range": [2, 4], "exclusiveRange": true };
var validate = ajv.compile(schema);
console.log(validate(2.01)); // true
console.log(validate(3.99)); // true
console.log(validate(2)); // false
console.log(validate(4)); // false
.addSchema(Array<Object>|Object schema [, String key])
compile
跟addSchema
非常像,其實我也分不太出來為啥要分兩個,
直接傳schema
var validate = jv.addSchema(schema)
傳schema並給予一個key值
var valid = ajv.addSchema(schema, 'mySchema').validate('mySchema', data);
除了addSchema
的話,也可以在宣告Ajv
時直接assignschemas
var ajv = new Ajv({schemas: [schema1, schema2]});
schemas: an array or object of schemas that will be added to the instance. In case you pass the array the schemas must have IDs in them
.addMetaSchema(Array<Object>|Object schema [, String key])
至於為何要addMetaSchema我還不是非常清楚
.getSchema(String key)
只透過uri來取得Schema
ajv.getSchema('http://example.com/schemas/schema.json')
我認為JSON-validator必須要有良好的message回傳機制才實用,所以我找到了一個我個人認為設計蠻好的套件jquery-validation,因為是jquery所以不太適合套用到reactjs專案,但是可以試著引用它的設計模式來打造一個適合自己專案的validator。可以看看Youtube上的jQuery Validation Plugin 播放清單來了解如何使用。
基本上它就是將驗證的rule與要回傳的message分成兩個property互相對應
遊戲感 Game Feel 有時也被稱為 Game Juice,指在玩遊戲時經歷的無形感受,最早在2008年在Game Feel: A Game Designer's Guide to Virtual Sensation這本書中提出。
遊戲感比較常用在動作遊戲、射擊遊戲上,一款最基本能動的遊戲,可以不需要任何特效與音效就能夠遊玩,但這樣的體驗非常的差,而我們可以一步步地添加細節,讓遊戲越來越精緻,回饋越來越好。
大部分的Game Feel都不會影響到遊戲最本質的玩法,算是幫遊戲穿上一層華麗的外衣,其中的感覺也很像繪畫時,一個圖層一個塗層的替畫作加上細節的感覺。
我是看了以下這麼多影片,才整理出這些概念的,感謝這些影片讓我長知識,我得說關於Game Design相關知識,國外的影片與文章比較多,台灣相對來說少,所以得多爬爬國外資源。
雖然上面已經整理出一些項目,但為了讓這些原始資料更原汁原味的呈現,以下是我把看過的影片分享給大家,大家有時間也可以每部都看過。
關於遊戲中打敵人打起來爽不爽,在【專欄】別再揮空氣了!關於遊戲中的「打擊感」三兩事有完整的解說,我覺得這篇太神到我不需任何註解,請大家直接看。
動作遊戲打擊感的來源是什麼?
https://www.zhihu.com/question/21342866
講者說:「好的Game Feel,沒人會注意到,但壞的Game Feel,大家會感受的到」
他用Demo,一步一步加上細節:
加入動作的影格動畫 - 就是移動時要有走路動畫,這沒什麼好說
加上音效 - 這邊比較特別的是音效會依據他出現的頻率去調聲音大小,像是頻繁的走路聲,音量就不要太大,然後同樣一個事件,每次可以播放有點不同的音效,這樣不會顯得太死板
Death Point - 標出玩家死亡的地方,可以用噴血噴漆等方式,可以讓玩家有成就感
死亡的特效 - 在死亡時加上ScreenShake、chromatic aberration(色差效果)、shockwave(波動)、真實的手把震動
其他 - 路邊的小石頭在你走路時會被你撞開、你的角色眼睛會一直看著敵人、背景會跟著音樂節拍震動、地上塵土在你走路或落地時會飄揚
關於音效:之前我朋友聽過思維工坊遊戲公司的音樂總監說:「有音效的作品才是從0分開始往上加,沒有音效的作品還不到0分」,所以音效是很基本該要有的元素
對最經典的Brick Pong遊戲加上一大堆細節,甚至開放原始碼讓大家回去嘗試,簡單明瞭。
Vlambeer公司開發者之一Jan Willem Nijman的演講,他們公司做出的作品節奏跟回饋感都很強,這場講座用Screenshake當作標題,而Screenshake也是加強Game Feel的其中一個方法,他在這場演講逐步加入每個效果並且Demo,是個非場精彩的演講。
以下我整理了,他提到的所有Game Feel要素,他以射擊遊戲為例
Game Maker’s Toolkit是個非常棒的頻道,講解蠻多知識的,我也是看這部才開始知道Game Feel的。以下是影片中提到的Game Feel效果。
如果你做的是射擊遊戲,可以加上後座力、開槍時就ScreenShake、增快開槍速度。
如果你做平台遊戲,可以加上摩擦力,讓玩家可以在牆上緩慢滑下,不要隨便亂動攝影機,讓玩家玩得不順。
這我沒全部看完,不過上方影片我幫大家調到講解Juicness的片段,跟其他影片差不多
本來想要自己生一段總整理,把每個要素分門別類,生出一些基本SOP讓人可以去檢驗自己專案的Game Feel,但還是太難了,這邊我只能整理出一個關於特效的心得。
在遊戲中加入大大小小的特效可以幫助遊戲更豐富,但啥時要加入特效,我認為物體與物體間的互動,都能加上效果,以更宏觀的角度來看,只要物體間有互動,都能加上效果,像是
總之,遊戲是沒辦法被所有SOP拆解的,就是有人可以想出體制外的玩法或故事才有趣,以上就是我的Game Feel分享。
jerryklu:
其實你也可以看成一個SOP,就是去模擬甚至過度強調所有動作在真實物理上的反應,有些需要強化是因為玩家不能感受到加速度的變化,所以要靠影像甚至是聲音過度強化的方式補足玩家感受不到的,但同樣是加速度,GTS跟蜘蛛人不可能有相同的強化方式,所以遊戲整體的風格、調性很重要。
cjcat2266:
我其中一個SOP是凡有動的東西,就用數值彈簧或easing,然後消除所有非刻意的視覺不連續性(popping)
https://codepen.io/collection/nYebQp
上方連結收錄了基本的23個Design Pattern,因為Design Pattern並不好讀,加上要原封不動地置入自己的專案中有點難,所以我覺得學習其中的設計精神與實際應用案例應該就夠用了,但是Design Pattern真的太難讀啦!所以我整理了一些較簡單的Javascript Sample Code跟說明在這邊,希望可以幫助到想學Design Pattern的人。
總體來說設計模式分為3大類:
工廠方法模式(Factory Method),抽象工廠模式(Abstract Factory),單例模式(Singleton),建造者模式(Builder),原型模式(Prototype)
適配器模式(Adapter),裝飾器模式(Decorator),代理模式(Proxy),外觀模式(Facade),橋接模式(Bridge),組合模式(Composite),享元模式(Flyweight)
策略模式(Strategy),模板方法模式(Template Method),觀察者模式(Observer),迭代子模式(Iterator),責任鏈模式(Chain of Responsibility),命令模式(Command),備忘錄模式(Memento),狀態模式(State),訪問者模式(Visitor),中介者模式(Mediator),解釋器模式(Interpreter)。
==================================================================
參考資料:
身為前端工程師,略懂UI設計好像也是挺重要的,在這邊整理一些網路上整理下來的UI設計的一些小知識,讓大家在設計介面與流程時可以參考。
關於UI Element的使用建議參考看看即可,因為UI的世界瞬息萬變,大家使用的方法也不盡相同,不過我覺得比較重要,也希望大家可以放在心上的概念是Microcopy,我認為這是不管在什麼地方都受用的觀念。
文件大綱:
參考資料:
In case you have less than 7 options you should consider using radio buttons. Your users will be able immediately scan how many options they have and what each of those options are, without clicking (or typing) anything to reveal this information.
Use Radio Buttons Rather Than Drop-downs
If possible, use radio buttons rather than drop-down menus. Radio buttons have lower cognitive load because they make all options visible so that users can easily compare them.
單個Switch與CheckBox看起來功用一模一樣,但有一個地方不同,先看看下圖。
當Switch Button按下時同時也會馬上儲存現在的設定,但如果需要按下儲存確認鍵才會更改設定,就要使用Checkbox了。
參考資料:
很多人常常拿Placeholder主要是在輸入欄位中提示「輸入範例」與「輸入限制」,有些人不太喜歡這種方式,在這邊提出另一種對Placeholder的看法
Rather than risk having users stumble while filling out forms or waste valuable time figuring out how they work, the best solution is to have clear, visible labels that are placed outside empty form fields.
Placeholder as an example ↓
Placeholder as help text ↓
Placeholder as secondary label(s) ↓
補充的文字直接放在placholder內的話,使用者在開始打字的時候如果忘記補充文字的內容,必須刪掉輸入內容後才能看到,考驗使用者的短期記憶力,所以像上面提到的,把說明文字放在欄位旁邊是個不錯的做法。
不過placeholder並沒有絕對的限制,如果整體使用情境不錯,還是可以打破上述規則,像是登入頁面的帳號密碼,因為大眾早已習慣兩個輸入框,上方是帳號,下方是密碼,所以就算直接把「帳號」、「密碼」寫在placeholder裡應該也不影響使用。
Nielsen Norman Group曾做過的一項眼球追蹤研究就說明,空的輸入框比有提示文字的輸入框更能引起用戶的關注,而且與空的輸入框相比,用戶更容易忽略有提示文字的輸入框,因為用戶乍看會覺得像已經填寫過的樣子。換句話說,被認為是有用的表單提示文字其實弊大於利,尤其是對習慣大致瀏覽的用戶,但我在瀏覽其他網站的時候,只要placeholder的顏色夠淡,感覺不太會有不順暢的感覺。
現在的Material Design的Adaptive Placeholders可以讓placeholder在輸入時轉換成標題,不過那沒有達到補充說明欄位的效果,所以在此不討論。
Adaptive Placeholders ↓
參考資料:
Microcopy是幾句簡短的文字,幫助使用者了解該做什麼或是舒緩對系統感到的困惑,達到的功效如下。以下依照功用分類進行介紹。
對使用者進行承諾,讓使用者安心。
承諾使用者不會濫用他的資料去twitter自動發文,或是對他的朋友發送垃圾信以及和自動關注帳戶 ↓
告知使用者取得他們資料的原因 ↓
提到這個就必須説說Information Icon了,因為有些資料輸入欄位很難從欄位名稱看出該輸入的什麼樣的資訊,所以通常會在輸入欄位的Label旁加上information icon,icon上會有是 ? 或 i 的樣式。按下icon的時候會跳出tooltip,詳細解說這欄位的用意或該輸入什麼。
Information Icon 的用處
Contrary to help hints, tooltips at first hide the information and then display it on demand. They are usually fuelled by an icon with a question mark. Help information is given by keeping on help icon or selecting it. Once the mouse goes far from the icon, the tooltips vanishes. These can effectively help in reducing clutter especially when the help text is lengthy.
Facebook 的 info icon ↓
Walmart 的 info icon ↓
Line Creator 的 info icon ↓
如果這個資訊相當重要,那就可以直接顯示文字資訊,而不要用info icon
If the information is extremely salient, consider displaying it at all times rather than as a tooltip.
參考資料:
當事情出錯時,知道發生什麼狀況有益解決它。如果你對錯誤的說明很模糊,想解決這些錯誤的使用者會很痛苦,如錯誤訊息的範例下圖。
錯誤訊息不要只寫「發生錯誤,請再試一次」這樣含糊的字句,你要想像你正在跟使用者說話,使用更好懂更具體的描述吧!
除了給予錯誤原因,最好給予解決方法,在下圖中,當登入失敗時,系統不只告知錯誤狀況,還提供了2個解決方法,後者的方法還提供了連結幫助使用者解決問題。
大部分網站對所有驗證狀態只會給給予一種錯誤訊息,然而MailChimp官方網站中填寫email欄位時會有3種不同的錯誤訊息,這種客製化的錯誤訊息我覺得最完美,但是也非常費工,我覺得在使用者最容易犯錯的欄位進行這種優化即可。
以上提到撰寫錯誤訊息的建議是從How to Write a Perfect Error Message這篇文章取出最重要的三點,其他說明可以點進去看看。
參考資料:
What is Microcopy: The small bits of text/copy that help instruct and alleviate(減緩) the concerns of your users.
By definition, the microcopy of a website consists of small chunks of text used in different elements – labels, buttons, error messages, tooltips, etc.
當你第一次使用系統時,有很多頁面都沒使用過,例如使用相片編輯軟體,但是沒建立任何檔案,此時畫面會是空白的一片,這就是所謂的空白狀態(empty state),請看 設計師最常忽略的 UI 頁面狀態:空白狀態(empty state),將會告訴你如何將空白狀態(empty state)處理的更完善。
Label與Field的關係也是一門學問,擺放的位置主要分成以下三種
普遍的狀況下最實用的一種排版方式,它方便觀看,如果把Label放在Field左側,眼睛注視表單時就必須在Label與Field之間左右來回移動,增加負荷,另一點是表單如果支援多國語系,label長度會不同,尤其法文的長度大致是英文的兩倍長。
優點
缺點
優點
缺點
如果要把Label放在Field左側,至少用向右對齊的版本
效果最差,眼睛要左看右看,負荷很大,應該算是最要不得的排版,雖然有人說在掃描整個Label時,因為靠左對齊的關係,比較好看,但整體起來還是不好用的。
在一般使用者填表的時候,我覺得盡量都用「Label在Field正上方」,但是在管理或是設定頁面時,有時候欄位很多(像是管理介面),想設定的欄位只有特定一兩個,並不需要從頭到尾每個欄位都填過,或許用「Label在Field左側 - 靠右對齊的Label」也是一種很好的選擇。若想使用「Label 在 Field 左側 - 靠左對齊的 Label」的方法,用有框線的表格更能幫助閱讀。
參考資料
避免使用"送出"之類的通用詞語來按鈕表示行為,因為它們給人的印象太通用了。相反地,請說明按鈕在點擊時執行的行為,例如"創立我的帳戶"或"訂閱每週優惠"。
按鈕位置雖然查到了一些研究與解釋,但似乎沒有個定案,以下大家參考看看就好。
在Dialog中,按鈕會被擺在右下角,因為放在右邊的東西有下一步的感覺,也符合閱讀的順序,而如果有Cancel與OK的多個按鈕,主按鈕應該放在最右側,也就是OK按鈕。
↓ User會看完所有按鈕再決定按哪個,所以主按鈕在左側會讓眼神多飄移一次
但是在整頁的大型的表單中(非Dialog),lukew的文章中建議把按鈕放左側是較好的方案,我覺得按鈕對齊按鈕input欄位應該是關鍵,因為user可能會回頭查看表單input的資料,再往下找按鈕的時候,也能較快速定位到按鈕。所以放左邊跟中間的按鈕較多,但在表單中把按鈕放右邊比較少。
參考資料
在登入頁面的帳密輸入框與搜尋引擎的搜尋Bar中常會實作按下Enter鍵就送出表單的功能,其他地方可以用這招嗎?我個人的看法是如果這方法不會改變資料的話就無所謂,但如果是新建或是刪除資料的表單最好還是不用比較好,雖然FB的發文就是按下Enter就送出,所以這方便大家就參考看看吧!
驗證分成兩類,而inline validation又再分兩類
有一派說法是贊成After Submit Validation,他們認為在user輸入資料時一直驗證會打斷user思考,user在填飽的時候會進入「填表模式」,當送出資料後,會轉換成「修改模式」去修正剛剛沒打對的地方,所以他們認為不應該打斷填表模式。
在使用者輸入某個欄位時,就進行驗證,不等到最後送出後才驗證,首先先來看兩種inline validation的驗證方式。
Validate during the data entry:
輸入資料的同時就會一邊即時驗證。
Validate after the data entry:
在輸入完資料後,離開這個輸入欄位或是當已經輸入到此欄位長度的最大值,才開始驗證。
在「Usability Testing of Inline Form Validation」文中提到如何做出最好的inline validation,我把結論整理在下方,
如果直接使用「Validate during the data entry」,使用者在點擊欄位或是才剛輸入第一個字元時就會看到錯誤訊息,user會覺得自己明明還沒打完,為什麼要被這樣糾正,體驗並不好。
解法:在第一次輸入的時候,記得使用「Validate after the data entry」,在使用者確實輸入完資料再驗證,體驗會較佳,如此便能解決常見錯誤1。
在「Validate after the data entry」後發現資料驗證錯誤,顯示錯誤訊息,而user回頭進行修改的時候,修改完後離開欄位(onblur event),系統會再度驗證,但是當我們修改資料時,可能已經通過驗證了,在等到離開欄位後再認證顯得不夠即時,這也就是常見錯誤2。
解法:修改錯誤資料時,使用「Validate during the data entry」,每輸入一個字元就驗證一次,一發現通過驗證就馬上隱藏錯誤訊息,如此一來就能達到良好的驗證效果。
某些狀況下,可以在資料尚未輸入完之前就驗證並給予警吿
參考資料
這份文件會告訴你如何使用jest與enzyme兩個工具來撰寫單元測試,並應用在你的react專案裡,你將會學會以下所提到的項目。
snap
檔,方便比對兩個DOM是否相同前端的單元測試在很多人看來都是一個可有可無的東西,理由一般有下面幾條(以下內容統一稱單元測試為單測):
其實,我大體上是同意以上觀點的。在大部分的情況下,如果公司的業務不復雜,是完全沒必要做單測的。但如果涉及到以下幾個方面,你就要考慮是否有必要引入單測了:
參考資料: React單元測試:Jest + Enzyme(一)
在撰寫單元測試的程式碼時,有個 3A 原則,來輔助設計測試程式,可以讓測試程式更好懂。3A 原則如下:
Jest是一個JavaScript的測試框架,也是所謂的Test runner,類似的項目大概有Jasmine(茉莉花), Mocha(摩卡咖啡), AVA這幾種測試框架,Mocha應該是最多人使用的,但後來Facebook延續Jasmine開發Jest,也是目前專案預設的測試框架,所以就繼續沿用。
因為Jest是新框架,所以也比Mocha多了一些新功能,或是讓語法更加精鍊。
jest-cli
模組,在react-boilerplate中已經安裝在devDependencies
內,相關設定也幫你設定好了。先寫一個簡單的function來測試,接著用pass.test.js檔案來寫測試程式,完成後在cmd下執行jest
,jest就會去所有目錄中找檔名是.test.js
或.spec.js
結尾的檔案來跑測試程式,把測試程式都放在根目錄中的__test__
資料夾也可以。
不過在我們專案直接下npm run test
就可以了,jest指令已經被整合進去了。
這是我們要測試的pass.js
const isPass = score => {
if(score>=60){
return true;
}else{
return false;
}
};
module.exports = isPass;
這是我們的測試程式pass.test.js
const isPass = require("./pass");
//describe中通常寫一個元件或是一個function
describe("function isPass()", () => {
//it是這個元件或function中的test case
it("should return true when score is 60", () => {
expect(isPass(60)).toBe(true); // 期待isPass(60)回傳的結果是true
});
it("should return true when score is 45", () => {
expect(isPass(45)).toBe(false);
});
});
參考資料:React 前端單元測試教學
jest 的一些斷言方法
// be and equal
expect(4 * 2).toBe(8); // ===
expect({bar: 'bar'}).toEqual({bar: 'baz'}); // == deep equal
expect(1).not.toBe(2);
// boolean
expect(1 === 2).toBeFalsy();
expect(false).not.toBeTruthy();
// comapre
expect(8).toBeGreaterThan(7);
expect(7).toBeGreaterThanOrEqual(7);
expect(6).toBeLessThan(7);
expect(6).toBeLessThanOrEqual(6);
// Promise
expect(Promise.resolve('problem')).resolves.toBe('problem');
expect(Promise.reject('assign')).rejects.toBe('assign');
// contain
expect(['apple', 'banana']).toContain('banana');
expect([{name: 'Homer'}]).toContainEqual({name: 'Homer'});
// match
expect('NBA').toMatch(/^NB/);
expect({name: 'Homer', age: 45}).toMatchObject({name: 'Homer'});
Stmts(Statement):
choice12.js
程式中的第8行,一行line中就包含console.log("statement_01"); console.log("statement_02");
Branch:
Funcs(Functions):
choice12.js
中只有一個Function,所以如果去測試那個function,覆蓋率就會100%。Lines:
Uncoverd Lines:
想要讓覆蓋率變成100%,只要將測試程式中的10,11,12行註解拿掉即可。
在react-boilerplate中,使用npm test
可以直接查看覆蓋率,一般情況則使用jest --coverage
查看,檢查的範圍可以在package.json
設定,如下
"jest": {
"collectCoverageFrom": [
"app/**/*.{js,jsx}",
"!app/**/*.test.{js,jsx}",
"!app/*/RbGenerated*/*.{js,jsx}",
"!app/app.js",
"!app/global-styles.js",
"!app/*/*/Loadable.{js,jsx}"
]
因為無法在react-boilerplate中似乎無法對「單一」測試檔進行jest --coverage,安裝jest-single-file-coverage方能使用。
在專案中的package.json
檔案中的script
屬性加上
"test:single": "node ./node_modules/jest-single-file-coverage"
就能使用npm run test:single <file_path>
對該路徑下的測試檔案進行測試,並顯示coverage
choice12.js
const choice12 = choice => {
switch (choice) {
case 1:
return 1;
case 2:
return 2;
default:
console.log("statement_01"); console.log("statement_02"); //沒被測試到
return 0; //沒被測試到
}
};
module.exports = choice12;
choice12.test.js or chocie12.spec.js
const choice12 = require('./choice12');
describe('function choice12()', () => {
it('should return 1 when enter 1', () => {
expect(choice12(1)).toBe(1);
});
it('should return 2 when enter 2', () => {
expect(choice12(2)).toBe(2);
});
// it('should return 0 when enter 5', () => {
// expect(choice123(5)).toBe(0);
// });
});
在command line下的Uncoverd lines大概只能顯示... 19,20,22,23
短短幾行,非常不方便,所以這邊分享觀看jest匯出的精美網頁版coverage報表。
在jest中測試覆蓋率後,似乎預設會匯出HTML檔案報表(不確定是不是react-boilerplate中
已經設定好的關係),在每次測試覆蓋率後,可以在根目錄看到coverage
資料夾,這個資料夾也在.gitingore
中被記載,直接開啟coverage/lcov-report/index.html
就能看到精美的覆蓋率報告。
觀看程式碼時,不同顏色與符號分別代表哪些資訊:
E
stands for 'else path not taken', which means that for the marked if/else statement, the 'if' path has been tested but not the 'else'.I
stands for 'if path not taken', which is the opposite case: the 'if' hasn't been tested.Nx
in left column is the amount of times that line has been executed.↓「I
代表的是if-else的if沒被執行,E
代表else的部分沒被執行」示意圖
以上資訊是參考類似的測試工具 Istanbul - a JavaScript test coverage tool
Snapshot testing is another new idea from Facebook. It provides an alternate way to write tests without any assertions. To write tests using assertions, Enzyme is quite useful.
快照(Snapshot)可以測試到組件的渲染結果是否符合預期,預期就是指你上一次錄入保存的結果,toMatchSnapshot
方法會去幫你對比這次將要生成的結構與上次的區別。使用snapshot,可以防止無意間修改组件的某些部分,以及快速增加測試程式的覆蓋率,剩下像是click事件這種不太能直接用snapshot比對的部分,再靠手動去撰寫程式檢測。
在這邊會使用enzyme,可以先到下一章了解enzyme
import React, { Component } from 'react';
import { render } from 'enzyme';
it('renders correctly', () => {
const wrapper = render(
<div id="helloworld">
<strong>Hello World!</strong>
</div>
);
expect(wrapper).toMatchSnapshot();
});
第一次執行測試的時候,只要遇到toMatchSnapshot
,就會在測試程式的同一層建立一個__snapshots__
資料夾,並且產生snap
檔,如果原來的測試檔案叫做index.test.js
,那麼所產生的snap檔為index.test.js.snap
index.test.js.snap
快照檔內容
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
id="helloworld"
>
<strong>
Hello World!
</strong>
</div>
`;
第二次跑測試程式的時候,就會去比對snapshot,如果比對結果有出入,就會跳出失敗訊息,當你想要更新snapshot時,只要執行jest -u
或jest --updateSnapshot
就可以了。u是update的意思。
特別要注意的是,如果你Component內容改錯了,還下jest -u
的話,就會把錯誤的DOM給記錄到snapshot裡面,所以在update之前記得要確認自己現在的程式OK才update。
如過只要測試單一的test.js
檔案,像是我如果想執行/app/components/choice
資料夾底下的測試程式,就在指令的地方下jest /app/components/choice
,就會只測試對應到的檔案,在cmd上會顯示Ran all test suites matching "/app/components/choice".
,同樣的,如果只要更新相關檔案的snap檔,就下jest /app/components/choice -u
就可以了。
註解: 個人覺得snapshot只能拿來當輔助,畢竟它只能比對現在DOM架構之前有無差異,但並不知道什麼才是"真正對的"架構。
以上的斷言基本是用在測試同步函數的返回值,如果所測試的函數存在異步邏輯。那麼在測試時就應該利用jest 的mock function 來進行測試。通過mock function 可以輕鬆地得到回調函數的調用次數、參數等調用信息,而不需要編寫額外的代碼去獲取相關數據,讓測試用例變得更可讀。
// 輸入一個數字,它會回你平方跟乘以3的數字
function getDoubleAndMultiplyby3(val, callback) {
if (val < 0) {
return;
}
callback(val * val, val * 3);
}
const mockFn = jest.fn();
getDoubleAndMultiplyby3(5, mockFn);
getDoubleAndMultiplyby3(10, mockFn);
describe("testing getDouble() with a mock function", () => {
it("it should was called least once", () => {
// expect(mockFn).toHaveBeenCalled();
expect(mockFn).toBeCalled();
});
it("it should was called twice", () => {
expect(mockFn).toHaveBeenCalledTimes(2);
});
it("it should was return 25,15 in first callback", () => {
// expect(mockFn).toHaveBeenCalledWith(25,15);
expect(mockFn).toHaveBeenCalledWith(25, 15);
});
it("it should was return 100,30 in last callback", () => {
// expect(mockFn).toHaveBeenLastCalledWith(100,30);
expect(mockFn).lastCalledWith(100, 30);
});
});
jquery
或cheerio
enzyme可以把它想像成一個react版本的jquery,使用enzyme中的shallow,就可以將react component轉換成像jquery一樣的物件,接著可以使用像是find之類的method取得所要的資訊進行比對驗證.
除了shallow以外,還有mount與render兩種method,但基本的shallow最常用。
mount
:Full Rendering,非常適用於存在於DOM API存在交互組件,或者需要測試組件完整的生命週期render
:Static Rendering,用於將React組件渲染成靜態的HTML並分析生成的HTML結構。render
返回的wrapper
與其他兩個API類似。不同的是render
使用了第三方HTML解析器和Cheerio
。簡易的Enzyme範例
import { shallow } from 'enzyme';
import MyComponent from './MyComponent';
import Foo from './Foo';
describe('<MyComponent />', () => {
// 檢查是否成功渲染一個MyComponent
it('renders <MyComponent /> components', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper).toHaveLength(1);
// expect(wrapper).exists()); //應該與上一行效果一樣
});
// 檢查MyComponent中是否成功渲染3個Foo
it('renders three <Foo /> components', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(Foo)).toHaveLength(3);
});
// 檢查MyComponent中是否成功渲染3個Foo
it('counter+1 when the last button is clicked', () => {
const wrapper = shallow(<MyComponent />);
//找出wrapper中的最後一個button,並且用滑鼠點擊
wrapper.find('button').last().simulate('click');
// ... expect(counter).toBe(1);
});
});
兩個重點:
async()
await sleep(毫秒)
以下的測試的情境是當error發生的時候,畫面下方的Snackbar會跳出並顯示錯誤訊息,2秒後自動消失。
open這屬性會從原本的false變成true,並在2秒後變回false,等待2秒這件事情我們使用await sleep(2050)
來實作,2050是因為2000+50,如果剛好用2000的話怕會有誤差,所以多50毫秒。另外,如果程式中使用到await
,則必須把程式包在async()
內。
it('set props to error, Snackbar should be opened', async () => {
const { wrapper, dispatch } = setup();
expect(wrapper.find(Snackbar).prop('open')).toBeFalsy();
wrapper.setProps({ error: 'RequestToken.Invalid' });
expect(wrapper.find(Snackbar).prop('open')).toBeTruthy();
await sleep(2050); // wait for more than 2000 ms(autoHideDuration)
expect(dispatch).toHaveBeenCalledWith({ error: '', type: 'app/Main/REQUEST_ERROR' });
wrapper.setProps({ error: '' });
expect(wrapper.find(Snackbar).prop('open')).toBeFalsy();
});
究竟Unit Test該測哪些項目呢? 以下是一些網路文章給的想法
作者整理出3個他覺得該測試的項目
單純的component測試因為沒跟其他物件以及資料有相依的狀況,較容易測試,而Container則相對複雜,我們可以看到下面的<App />
被包在很多層DOM裡面,如果缺乏這些外層DOM的資訊,將很難對<App />
中的Container進行測試,解決方式就是模擬外層DOM將這些模擬的資訊傳遞給裡面的DOM。
<Provider store={store}>
<MuiThemeProvider muiTheme={getMuiTheme(customizedTheme)}>
<LanguageProvider messages={messages}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</LanguageProvider>
</MuiThemeProvider>
</Provider>
Provider
:提供storeMuiThemeProvider
:提供Material UI的資訊LanguageProvider
:真正的Provider是IntlProvider
,提供跨國語言包ConnectedRouter
:負責記錄history解決方法就是建立一個enzymeHelper.js
檔案,在裡面實作mountWithProviders
取代enzyme原生的mount
,在mountWithProviders
執行enzyme的mount
,並將所有要傳給Container的資訊放進context內,建立了出來的wrapper就能夠正常運作。
enzymeHelper.js
import React from 'react';
import { shallow, mount } from 'enzyme';
import { IntlProvider, intlShape } from 'react-intl'; // mock IntlProvider in LanguageProvider
import getMuiTheme from 'material-ui/styles/getMuiTheme'; // mock MuiThemeProvider
import ReactRouterEnzymeContext from 'react-router-enzyme-context'; // mock ConnectedRouter
import configureStore from 'redux-mock-store'; // mock Provider
// set up Provider
const store = configureStore([])({});
store.dispatch = jest.fn();
const dispatch = store.dispatch;
// set up intlProvider
const messages = require('translations/en.json');
const intlProvider = new IntlProvider({ locale: 'en', messages }, {});
const { intl } = intlProvider.getChildContext();
// set up MuiThemeProvider
const muiTheme = getMuiTheme();
// set up ConnectedRouter
const router = new ReactRouterEnzymeContext().get().context.router;
// assign props 'dispatch' into node, node is the component which we want to test.
function nodeWithProps(node) {
return React.cloneElement(node, { dispatch });
}
// pass down the context of Provider, intlProvider, MuiThemeProvider and ConnectedRouter to shallow
export function shallowWithProviders(node) {
return shallow(nodeWithProps(node), { context: { intl, muiTheme, router, store } });
}
// pass down the context of Provider, intlProvider, MuiThemeProvider and ConnectedRouter to mount
export function mountWithProviders(node) {
return mount(nodeWithProps(node), {
context: { intl, muiTheme, router, store },
childContextTypes: {
intl: intlShape,
muiTheme: React.PropTypes.object,
router: React.PropTypes.object,
store: React.PropTypes.object,
},
});
}
enzymeHelper.js的使用方法
import React from 'react';
import { mountWithProviders } from 'enzymeHelper';
const defaultProps = { 'test': "testVaule" }
const setup = (props = {}) => {
const wrapper = mountWithProviders(<MyContainer {...defaultProps} {...props} />);
const actions = {
testMethod: wrapper.instance().testMethod,
testOtherMethod: wrapper.instance().testOtherMethod,
mock: (...methods) => { //you need to implement mock() in every test.js
methods.forEach((method) => {
wrapper.instance()[method] = jest.fn();
actions[method] = wrapper.instance()[method];
});
},
};
return {
wrapper,
actions,
dispatch: wrapper.props().dispatch,
// customize common DOM you want to test
// ex: loginBtn: wrapper.find(FlatButton).at(0),
}
};
describe('functions of <MyComponent />', () => {
it('testMethod()', () => {
const { wrapper, actions, dispatch } = setup();
// testMethod() include a dispatch
actions.testMethod();
// check the testMethod() trigger dispatch or not
expect(dispatch).toHaveBeenCalledWith({
data: { testContent: null, type: 'Text' },
type: 'app/MyComponent/TEST_METHOD',
});
it('click button to trigger testMethod()', () => {
const { wrapper, actions } = setup();
// mock testMethod()
actions.mock('testMethod');
/*
* if you want to mock multiple functions
* you can assign mutiple parameters into actions.mock()
* ex: actions.mock('testMethod','testOtherMethod');
*/
wrapper.find('button').first().simulate('click');
// testMethod() will be called once after Clicking button
expect(actions.testMethod).toHaveBeenCalledTimes(1);
});
直接使用下方的Template開始開發Container的單元測試
// *** REMOVE THE COMMENTS IN THIS FILE TO START CODING UNIT TEST ***
/*
* ComponentName <= replace ComponentName with the Component you want to test.
* FunctionName <= replace FunctionName with the function you want to test.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { mountWithProviders } from 'enzymeHelper';
/* TODO: import the component you want like material-ui TextField or FlatButton */
import { ComponentName } from '../index';
const defaultProps = {
/* TODO: need to put default props here */
};
const setup = (props = {}) => {
const wrapper = mountWithProviders(<ComponentName {...defaultProps} {...props} />);
const actions = {
/* TODO: customize common DOM you want to test */
mock: (...methods) => {
methods.forEach((method) => {
wrapper.instance()[method] = jest.fn();
actions[method] = wrapper.instance()[method];
});
},
};
return {
wrapper,
actions,
dispatch: wrapper.props().dispatch,
/* TODO: customize common DOM you want to test */
};
};
describe('<ComponentName />', () => {
it('match snapshot', () => {
const wrapper = shallow(<ComponentName {...defaultProps} />);
expect(wrapper).toMatchSnapshot();
});
it('must have ......', () => {
const { wrapper } = setup();
// TODO: check if the important DOM exists ex: button, input, table
// ex: expect(wrapper.find(FlatButton)).toHaveLength(1);
});
// TODO: check events executing correctly or not, ex: click,change,keypress
// TODO: check UI changing correctly or not when different props assign
});
describe('functions of <ComponentName />', () => {
it('FunctionName()', () => {
const { actions } = setup();
// if you want to mock FunctionName(), then use actions.mock('FunctionName');
actions.FunctionName();
// TODO: check the result correct or not
});
// TODO: test other functions
});
Component的單元測試Template
//直接使用下方的Template開始開發Container的單元測試
import React from 'react';
import { shallow } from 'enzyme';
// TODO: import the component you want like material-ui TextField or FlatButton
const defaultProps = {
// TODO: need to put default props here
};
import CheckboxOption from '../index';
describe('<CheckboxOption />', () => {
it('match snapshot and must have ......', () => {
const wrapper = shallow(<CheckboxOption {...defaultProps} />);
// TODO: check if the important DOM exists ex: button, input, table
// ex: expect(wrapper.find(FlatButton)).toHaveLength(1);
});
// TODO: check events executing correctly or not, ex: click,change,keypress
// TODO: check UI changing correctly or not when different props assign
Saga的測試方式跟UI的測試方式有點不同,所以在這邊解說該如何測試,其實主要就是把原本saga.js
中所有yield後的put
,call
等相關指令全部測過一次。
api.js
const api = {
fetchProductAPI() {
return 'iphone';
},
};
export default api;
saga.js
import { call, put } from 'redux-saga/effects';
import api from './api';
export default function* fetchProduct() {
try {
yield call(api.fetchProductAPI);
yield put({ type: 'PRODUCTS_RECEIVED', product: 'iphone' });
} catch (error) {
yield put({ type: 'PRODUCTS_REQUEST_FAILED', error });
}
}
saga.test.js
/* eslint-disable redux-saga/yield-effects */
import { put, call } from 'redux-saga/effects';
import { cloneableGenerator } from 'redux-saga/utils';
import fetchProduct from './saga';
import api from './api';
describe('fetchProduct()', () => {
// gen = fetchProduct(); <== original style
const gen = cloneableGenerator(fetchProduct)();
it('try', () => {
const clone = gen.clone();
expect(clone.next().value).toEqual(call(api.fetchProductAPI));
expect(clone.next().value).toEqual(put({ type: 'PRODUCTS_RECEIVED', product: 'iphone' }));
});
it('catch', () => {
const error = 'product not found';
const clone = gen.clone();
clone.next(); // <== before throw error, you need to execute gen.next();
expect(gen.throw('product not found').value).toEqual(put({ type: 'PRODUCTS_REQUEST_FAILED', error }));
});
});
Redux-Saga也有Library,其中星星數最多的框架是redux-saga-test-plan (415 star),但是目前一直套用失敗,有時間與需求的話再回頭研究,有個網站分析各個Redux Saga Test Library。
generator.next(value
)中的value該填入什麼才對呢?
可以看到下面的saga.test.js的第3行中generator.next(product).value
,這個product
填這邊的原因要看到saga.js,saga.js中第2行的yield put({ type: 'CALL_API' });
是接續第1行的const product = yield call(api);
,所以要將product
填在next中讓之後的測試程式可以用到product
。
saga.js程式片段
const product = yield call(api);
yield put({ type: 'CALL_API' });
saga.test.js程式片段
const prouduct = "mock product data";
expect(generator.next().value).toEqual(call(api));
expect(generator.next(product).value).toEqual(put({ type: 'CALL_API' }));
有時候你的 saga 可能會有不同的結果。為了要測試不同的 branch 而不重複所有流程,你可以使用 cloneableGenerator utility function,他可以複製某一步驟的generator,範例如下
import { cloneableGenerator } from 'redux-saga/utils';
// 原本的程式是 const gen = getOPList(action);
// 如果要複製品這個generator,就改寫成下面這行
const gen = cloneableGenerator()();
it('use clone', () => {
//將剛剛的gen複製一份成clone
const clone = gen.clone();
expect(clone.next().value).toEqual(put({type:"ohya"})));
});
想了解整段程式怎麼寫的可以參考完整範例程式,其中要跳到catch的case中,就必須使用throw()
,但在throw()
之前我們必須先執行一次next()
才行,error部分的測試程式如下。
it('catch', () => {
const error = 'product not found';
const gen = fetchProduct();
gen.next(); // <== before throw error, you need to execute gen.next();
expect(gen.throw('product not found').value)
.toEqual(put({ type: 'PRODUCTS_REQUEST_FAILED', error }));
});
基本上flow function都使用takeEvery,目前測試遇到takeEvery都會失敗,後續再研究如何測試takeEvery,或是根本不需要去測試。
觸發event的方式會因應你使用shallow
或mount
會有所不同,下面以onChange
事件為例寫下兩種不同範例。
shallow
const input = wrapper.find(TextField);
input.simulate('change', { target: { value: 'newValue' } });
mount
const input = wrapper.find(TextField);
input.prop('onChange')({ target: { value: 'newValue' } });
取得Dialog中actions裡面的按鈕
wrapper
.find(Dialog)
.prop('actions')
.forEach((btn) => {
btn.props.onClick();
});
取得Dialog內Component的方法:中的內容無法直接使用find來抓取的,所以我們用prop('children')來抓取,但隨著每個Dialog內部有著不同的
const dialog = wrapper.find(Dialog);
dialog.prop('children')[0].props.onChange();
const dialog = wrapper.find(Dialog);
dialog.prop('children').props.children.forEach((el) => {
if (el.props.children) {
el.props.children[0].props.leftCheckbox.props.onCheck(null, true);
el.props.children[0].props.leftCheckbox.props.onCheck(null, false);
el.props.children[0].props.nestedItems.forEach((listItem) => {
listItem.props.leftCheckbox.props.onCheck(null, false);
listItem.props.leftCheckbox.props.onCheck(null, true);
});
}
});
教學影片:
文件與網站:
研究中的題目
react-happy-validator的原始碼與說明都在下方連結
https://github.com/Hsueh-Jen/react-happy-validator
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.