2018年7月6日 星期五

[翻譯][Video][統計]Steam遊戲到底賣的如何?


2017
每款遊戲賣500套 (500中位數, 3700平均)
第一個月營收$2000中位數,$18000平均.
第一個月平均價 $7 中位數, $8.3 平均數

2018
每款遊戲賣50套 (50中位數, 2000平均)
第一個月營收$250中位數,$2000平均.
第一個月平均價 $5 中位數, $7 平均數

2017.08
每天出25款新遊戲
2018.02
每天出40款新遊戲

除去粗製爛造的遊戲 2018.02
每款遊戲賣2000套 (2000中位數, 10000平均)
第一個月營收$12,500中位數,$110,000平均.
第一個月平均價 $12 中位數, $13 平均數

第一年營收預估
第一個月營收 * 2.5 = 第一個星期營收 * 5 = 第一年營收 $30,000

2018.02 early access的遊戲
每款遊戲賣3000套
第一個月營收$24,500中位數
第一個月平均價 $15

2018.02 有發行商的狀況
每款遊戲賣6000套 (3倍)
第一個月營收$61,250 (5倍)
第一個月平均價 $15

2018.02 定價和銷售狀況
大於$15
每款遊戲賣5,000套(中位數)
第一個月營收$70,000(中位數)
$8-$14
每款遊戲賣1,000套(中位數)
第一個月營收$7,000(中位數)

93%開發者賺不到生存下去的錢!!

2018年6月9日 星期六

[翻譯]你的遊戲到底是什麼?

http://gamasutra.com/view/news/193338/What_is_your_game_actually_about.php

這是2010 Game Developer magazine的文章,Civilization IV Lead Designer Soren Johnson談論遊戲的風格及意義的差別


誰來決定遊戲到底是什麼?


看到桌遊Ticket To Ride的第一眼,你可能覺得又是個超級鐵路連線的遊戲,類似Age of Steam, Eurorails, 1830系列。在遊戲中,玩家會挑戰將兩座城市用鐵路連起來。紐約到舊金山、邁阿密到芝加哥等。

為了完成遊戲,玩家需要建設一連串的鐵路將各城市相連,同時也要阻礙對手完成鐵路。也有次要目標,像是完成最長的鐵路或第一個完成網路等。

然而大多數的玩家認為Ticket To Ride是個挑選路線並同時阻礙對手的遊戲。但是背景故事是另一回事:"在秋風颯颯的早晨,5位老朋友約在城市的一個隱密的小包廂。每個人都是從世界各個角落長途遊行過來的,為了這特別的一天。1900/10/2。28年前的今天,Phileas Fogg贏得了賭注20,000英磅,因為他在80天內環遊了世界一週。"

背景故事參考

接下來每年他們都碰面慶祝並且付獎金給Fogg。且每年都提出新的探險(總是越來越難)。在世紀末的今日,是該來趟新的冒險了。賭注是100萬美金的競爭。看誰能在北美七日內藉由鐵路經過最多的城市。

這官方的故事令許多人感到驚訝,甚至是遊戲的老手,因為這風格根本完全和遊戲方式不搭。例如:為何玩家要搭建自己的鐵路?是因為有人把鐵路關閉,防止其它人來使用鐵路?或者是有不擇手段的大富豪為了獲勝而嘗試去控制鐵路?

不論如何鐵路是可以被佔據的,這和現實世界的印象、物理法則完全不搭。取而代之佔據鐵路反而比較像打算買下它而不是利用它來旅遊。

遊戲機制帶來意義


這個差異帶來許多有趣的問題。一個遊戲設計者有資格決定這遊戲是什麼樣子嗎?而玩家感覺又是另一回事?如果設計者沒這個權力,那遊戲的背景故事到底代表了什麼?遊戲是否真如玩家所體驗?

最終來說,設計者必須了解遊戲風格並無法決定它所帶來的意義。反倒遊戲的意義是從機制衍生,這一連串的抉擇及結果對每個玩家造成影響。這遊戲到底對玩家問了什麼問題?它懲罰了什麼?又獎勵了什麼?什麼樣的策略及方法是遊戲所獎勵?回答過這些問題才能真正顯示出你的遊戲是什麼。

更進一步思考,當人們為了其風格("我想當個宇宙特戰隊")買了款遊戲,而遊戲機制中得到了樂趣("事實上是射擊外星人")。當這兩者有強烈的差異時,玩家會覺得被欺騙了,認為是設計者只是想騙玩家錢。

關於Spore的接受度,它提供了"物種進化"的風格,提供了比較近代的例子。在2008十月的科技雜誌上,John Bohannon寫了篇他如何保證遊戲風格的文章:"我已經與團隊中的分析人員玩過了遊戲,確定了其各方面符合科學角度。這絕對符合生物學及演化論,但是Spore完全失敗了。根據分析,問題並非出在Spore在科學角度的分析,而是出在其遊戲機制。甚至它讓生物學變得沒意義"。

讓這兩者差異如此明顯的原因於Spore雖然以"物種進化"的風格為賣點,但其遊戲機制並非真的關於進化。Spore的遊戲事實上是創造-玩這款遊戲通常是想用編輯器造出他們想像中的生物,而非是遊戲設計者所想像,由樂器產生優美的音樂而進而營造出其環境。

無論如何,即使Spore雖然不是關於進化,而分析人員關注另一款真正的進化遊戲-"魔獸世界"。他的風格也許是劍與魔法的世界,但他的機制鼓勵玩家去改變他們的角色來克服環境上的挑戰。

經過了數年的更新經驗,WOW的老手確立了各種版本的職業配裝來符合其角色定位。例如:聖騎有三項天賦樹:Holy(治療),Protection(坦),Retribution(DPS)。更進一步,除了主要的分類,次要的分類如PVP,PVE,刷寶等。這些分類被發展出來,源自於這遊戲獎勵的玩家行為。

回顧過去


回顧過去有些遊戲機制影響玩家體驗的例子。超級瑪莉:事實上是抓時機的遊戲,而不是水管工。Battlefield是有關團隊合作,而不是WWII或現代戰爭。Peggle是混沌理論,而不是獨角獸或彩虹。

事實上,相同風格的遊戲事實上可能是不同東西。例如:人類對抗異形在電玩史上是個很熱門的題材,但是每款對抗異形遊戲,因規則的不同可能有極大的差別。Galaga(小蜜蜂)是在找圖形規律,而X-COM是以有限資訊的決策遊戲。戰爭機器是利用掩體為防禦武器。星海是挑戰非對稱戰爭。

反過來說,遊戲雖然有不同風格但是機制相同則是表是同件事。文明帝國及Alpha Centauri雖然在不同星球,但是其機制大致上的一樣的。Alpha Centauri的心靈蠕蟲、飛船小隊及機密任務剛好和文明帝國的蠻族、間諜、世界奇觀一樣。玩家可以從過去的遊戲脈絡發現在做相同的決策及相同的利益評估。

例如兩個最近的遊戲:Halo Wars, Brutal Legend,都是策略(strategy)遊戲。大家期望前者Halo Wars是靠敏捷反應為主的戰鬥。而後者重金屬風格則不像是一般策略。因為策略遊戲常常玩起來和想像中不太一樣,玩家因為風格所影響,反而造成遊玩失望。設計者建立了有趣且吸引人的組合,但風格確讓遊戲賣給了不同的玩家。

讓風格和機制一致


有個有趣的例子是桌遊Risk和Diplomacy,它有著一致的世界征服風格。事實上,第一眼看起來兩個遊戲有著相同的機制。遊戲版圖因勢力而分佈,玩家可以控制一般軍隊或海軍。玩家輪流操作不同勢力戰爭,勝利者通常控制了版圖上最大的軍隊。

但是規則有個小小的差別,讓這兩個遊戲變得非常不一樣。在Risk中回合是依照輪流進行,而Diplomacy則是同時進行回合。這個差別讓它們成為了不同的遊戲。在Risk,玩家依自己回合能做的最大利益做決定,並希望骰子能符合自己的相法。在Diplomacy,他們是沒有骰子的玩家只能依靠彼此的幫忙,而這些幫助只有口頭同意,而沒有遊戲內確定的協議。只有當密秘身份為揭露時才能真正確認誰是友人、誰是叛徒。

Diplomacy完美地結合了其風格及機制,事實上這還是甘迺迪總統最喜歡的遊戲。這遊戲和它宣傳地一模一樣,衝突及外交協議!

2018年5月3日 星期四

[Unity][教學][翻譯][筆記]動態尋路Part1

原址:https://www.youtube.com/watch?v=4Kj6YUPLWCw

首先來看最後成果
最後成果為藍球走到藍色目標、紅球走到紅色目標。

#1 設置好你的場景
我將場景靜態物件都放在同一物件Map之下,之後可以一同設定。
然後挑一個紅球當Agent、另一個紅色為目標物
其它顏色及數量最後再複製就好

#2 設定好靜態物件
將你所有的場景物件設為靜態,即為右上角的選項Static
事實上主要是要設定Navigation Static,右鍵點擊Static可以看到

#3 Bake Navigation Map

由上排選單列打開Window/Navigation
參數有空再看文件調整,
直接按下Bake就會產出Navigation Map了.
你可以在scene的視窗看到他畫出的圖,淺藍色即是可以移動的路徑

#4 將NavMeshAgent指定給紅球


#5 寫個簡單的script Agent控制NavMeshAgent

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Agent : MonoBehaviour {
    public Transform target;
    private NavMeshAgent navAgent;
// Use this for initialization
void Start () {
        navAgent = GetComponent<NavMeshAgent>();
        navAgent.SetDestination(target.position);
}

// Update is called once per frame
void Update () {

}
}

#6 將Agent也塞給紅球
並目標Target設定好

#7 按下執行

若想測試多個只要再複製紅球
及再用同方法製造藍球即可

範例:https://github.com/MageWang/Crowd-Behaviours-on-a-Dynamic-Navmesh-in-Unity-Part-1-Sample
會寫這篇只是想留個筆記

2018年4月20日 星期五

[翻譯]Rogue Legacy設計筆記

原文網址:
http://cellardoorgames.com/rogue-legacy-design-notes/





這些不是你所想像的正規設計文件,它們不只難以閱讀、而且有很多缺陷。這也不是一般的遊戲製造流程,我們是一對兄弟在家裡工作,我們用SKYPE去和美術及音效溝通。若你將其應用在更大的團隊上,事情可能不會進行的這麼完美。我們同時也蒐集很多的素材。

Rogue Legacy需要兩週的時間來開發基本引擎及關卡編輯工具來製作這款遊戲。主要的設計文件都是在這時期設計的。所以這遊戲的內容及工具同時間被一起做出來,像是雞生蛋問題一樣。

很多的文件只是初期設計,額外的文件是在Rogue Legacy開發期間一邊進行的,但是有更多特別的狀況是在產品階段被發現的。這些並沒有特定的時間順序,我們也不知道它們的正確順序是什麼。

Teddy Lee
Creative Designer

#1

一開始這款遊戲叫做Rogue Castle,之後我們將名字改為Rogue Legacy,讓它更貼近遊戲本意。我現在還是滿喜歡Rogue Castle這名字。

這是最早遊戲各個狀態的設計。有兩項數值之後被移除了體力(Stamina)及移動速度(Move Speed),體力是個玩家揮砍的可用量,限制玩家無用的揮砍。移動速度則被移到符文系統。玩家本來是通過擊殺怪物來取得經驗值。經驗值可用來升級技能樹。而這些技能被稱為特徵,因為你利用經驗值去強化你基因的強度。這頗令人混肴。這個名稱在開發期造成很大的困擾。

本來的金錢可以兌換技能及符文,被改成金錢兌換符文、經驗值換特徵,而且還有些特徵是這邊看不到的(例如:色盲、肌肉萎縮)。

由於我們曾經更改過命名,我們兩個的溝通常常不是很順利。我們名字用了越久,要更改它的難度越大。所以如果你要做款遊戲,你的系統是很相似但是卻又不一樣的,最好在一開始就確定其術語。

雖然你也許不相信,Rogue Legacy不是我們第一次做城堡升級的系統。我們第一次做這個系統是在Villainous、一個守塔遊戲。我們需要做個相似的系統在RL。這有很多工作需要做,所以與其做個複雜的城堡,我們決定做個相對簡單的樹狀結構。(我們也參考了族譜的樹狀)。我們決定停止Villainous,它是個可完整成長的莊園。因為它是個完整遊戲的一部份,所以早期稍有不注意就會造成錯誤。

因為這是個容易死亡的遊戲,升級畫面也同時出現在這時間點。

當你死亡時,家族傳記同時也被當成排名榜。我們最後只稱呼其排行榜,讓其比較容易視覺化。

#2

這份筆記描述更多有關我們的舊的技能樹。

Rogue Legacy的技能樹原始設計其它是像是Kingdom Rush,而我們稱呼技能為"圖騰"。當你放置技能在較低階的圖騰,更高階的技能就被解鎖。

這系統現在看起來很奇怪,但是回頭來看遊戲的主要目標一直沒被設計。我們想要讓擊倒各個頭目會得到極大的獎勵,所以他們本來會解鎖更多圖騰讓玩家來裝備技能。

接下來是圖騰技能的獎勵,這並不是完整列表,因為早期設計還不需要這麼明確的列表。取而代之,更重要的是讓玩家知道你未來想要的技能什麼時候能取得。讓長期規劃變得更加重要。

#原始的技能樹設計。這還真的是棵樹。

你可以看到筆記上我們比較注重被動技能。但即使他們是被動的,每項被列出的技能都有其特別的實作方式,他們有其各自特別的實作方法(雖然我不是工程師)。

例如:

-14:陷阱的傷害降低,這表示我們需要能特別調降環境造成的傷害。
-19:更高的掉寶率,這表示我們需要一個公式來調整寶物的取得及機率。
-20:減傷系統,這包含了對所有傷害的抵抗力或是對特定傷害的防禦力。
2,3,4 我忘了這到底在寫什麼,看來十分愚蠢。

#3

更多關於技能樹的筆記。在右上方可以看到我所畫的遊戲循環。

Play->Fun+Change->Death->RPG

這個循環是我們一開始就打算做的。分析所有遊戲內的機制都是為了滿足這份體驗,像是你在其它RPG所遇到的一樣。這是一小部份的筆記,但這是真的很重要的一部份,這也是為什麼它會被寫下來。

這裡也有寫下特徵系統為是如何被看待的。它們看起來像是圖騰。

我們也有記下角色職業的筆記。所以你可以看到原始概念,騎士可以擋住傷害、忍者可以瞬移。我們也有另外的職業可以衝刺攻擊,但在早期的設計被砍掉了。衝刺移動為了重新滿足符文系統後來被加回去。

在這你也可以看到原始的遊戲控制設計。B鍵是職業的特別能力,Y是藥水(我們本來打算設計讓玩家可以有可消耗道具)。

大多的項目在最後版本都被徹底簡化。像是以上所說的,衝刺變成符文,Y鍵變職業特別能力,技能變成單鍵使用而不是方向鍵加動作鍵。而藥水成為臨時消耗品。

這些大部份的原始設計大多沒有成為產品。我們常在重新檢視我們的遊戲,並將某些項目開始做之前就刪除。這也是另一個原因,為什麼我們不看重設計文件。它讓我們的行動不再敏捷。

很多人說實作雛型是重要的,但同時給予雛型方向也是很重要的。如果你做的事只有10%機會放到遊戲內,則這是浪費時間的。所以將某些意見刪除是可以省下很多時間及金錢。

#4

這份文件描述地圖生成器是如何運作。

上面的字都很簡單,關鍵字足以勾起我的回憶。我的經驗中,一長串的文字解說未必有幫助。有以下幾點原因。

1 可以節省設計者的時間
2 這只有設計者會讀這文件
3 這限制了創意的發想
4 這不是個理想的資料給程式或美術。因為我並不是專長於那些領域,所以寫下我所知道的細節,對工作並不會有幫助。

當這些事被寫下來,這同時也賦與了這些意像其創造力。因為它是真正被正式記錄下來,而不是只是隨口說說。我知道這聽起來很好笑,但這是真的。至少我認為是真的,對我而言。

同時,事情也從未照所寫下的執行。這也幾乎不可能去預測這個點子多困難被實現,這最好能保持單純。這樣當另外的團隊成員加入時,可以隨時更改它。同時,人們也總時很懶散,而非愚蠢。他們總會將點子的印象記在腦子,如果他們忘記了,沒有人能從60頁的厚重設計文件內去找出一個正確的答案。

** 重申一次這份文件在製作時持續改進,而且我也從未在5人以上團隊工作。

#5

這份筆記詳述了舊職業及特徵系統。本來基因特徵系統並未存在於遊戲內,特徵有點像是天賦。一個角色可以擁有主要加強法力的天賦,而副天賦力可以小幅強化其它能力。這雖然有點複雜,但這就是在我們重新建構新系統前的原始天賦系統(新的是基因系統)。這是少部份被移出正式版的特色,另一方面它也讓工作變得相當麻煩。

這遊戲同時也有可以多次升級職業。下面的例子是"礦工、冒險家",而騎士及法師就沒有舉例了,因為他們是相業單純的職業。我們必須先確定這個系統能應付最複雜的職業需求。這份筆記上,冒險家能強化藥水能力。

這算是非常無用的能力。

#6

這頁描寫的很單純是整個遊戲的概略設計,你可以在腦中視覺化這些項目,且你的筆記會涵蓋這些項目。這頁算是一種思緒的整理,像是重點筆記,這歸納了我的所有想法,確保沒有遺漏的部份。

歸納自已的點子也是個將工作確定的好方法,因為這可以強迫自己去想過一次整個遊戲的各個項目。你會發現其中多餘的設計。歸納可以讓你整合及移除遊戲中不需要的元素。

這頁的下方簡述了戰鬥,而更下方則是初始關卡結構。

我們知道自己想要找到方法去固定部份遊戲,但又要能提供每次遊玩的不同體驗。初始想法是擊倒一個魔王後有部份的關卡會被鎖定。這想法很早就被丟棄了。理由如下:
1 太難去實作
2 太難讓玩家瞭解
3 太愚蠢的點子

#7

這額外的筆記你可以看到更多天賦的例子(例如:舊的特徵)

這天賦下方是我們的新消費系統。之前有提到Rogue Legacy原本擁有XP及金錢系統。金錢可以升級圖騰(後來的莊園)。這個金錢即使死亡還是會存在,XP則會被重置,除非你拿去升級,否則死亡時所有XP都會歸零。

每次升級你的基礎能力都會上升,而且這有個公式確保玩家可以不斷升級。這個系統太複雜了,而且我們遊戲要用XP升級,看似不太適合。

這是個困難的抉擇,但我們決定最佳方案是拿掉其中之一的消費系統。所以我們把XP給拿掉,將金錢變成唯一的升級方式,而且金錢也取代原來的XP作用。

這個改動造就了我們最具代表性的想法之一。舊的XP系統提供了rogue-like的概念,因為每次你死亡你就流失了所有XP。我們不能對金錢做同樣的事,所以我們必須用其它方法解決。最終決定建造這座城堡。甚至將死亡掉落金錢給拿掉,迫使你在重新進去城堡內前,花光身上的錢。

最後一點,我真的討厭砍去XP系統。不只因為我喜歡它,因為我曾經加入了它,我應該更有遠見去考慮到這是不適合的。我並沒有仔細去想過,我應該可以避費浪費許多開發時間。

XP是另個被完全砍去的系統。XP內的項目也沒有被保留在遊戲的其它部份。連帶受影響的不只設計者,而是整個團隊。所以嚴格看待你提出每個點子。

#8

這個篇幅比較短。只是記下通常的平衡問題。我一開始希望

玩家技術 >>>>> (遠大於) >>>>>>  遊戲時間

但我們仍然希望即使玩家技術不好的人可以過關,所以花時間累積是個可行的選項。這面就是在說如何管控玩家傷害、生命等。確保玩家在短期遊玩時不會感到突兀,但長時間會對遊戲有影響。

#9

有關Fairy Chests的概念寫在這裡,連帶發展出符文系統。

符文系統允許玩家將用類型的符文裝在身上,與能力相乘成長。

原本符文系統的點子是比字面上更加強大。例如:一種符文是給予陷阱無效,而加上額外符文時會再有另一項傷害無效。當然不能強到過頭,所以我們限制符文的上限是5個。這仍然使技能無效的點子太過複雜,不只針對程式,還包含如何顯示給玩家。所以我們拿掉所有傷害無效的符文功能,為了更加簡潔的系統。

陷阱無效最終成為了特別的道具(飛鞋)。
#Hermes boots宙斯之子Hermes的鞋子,法國時裝品牌愛馬仕。

飛鞋讓故事更多了些浪漫的氣氛。因為我們會不斷重複遊玩,所以我們決定拿掉有特別背景的技能,讓我們有更多的創作空間。

#結尾

就這樣。希望從這些枯燥的東西帶給大家些樂趣。

2018年2月7日 星期三

[翻譯]製作20XX的故事(上)

原文:
https://www.gamasutra.com/blogs/ChrisKing/20171115/307769/Making_20XX_Listening_Well_and_Lucking_Out.php

嗨,我是Chris King,我最近釋出了遊戲20XX。20XX是個洛克人X、ROUGE-LITE、結合傳統橫向捲軸動作(classic action platforming)及自動生成關卡(random level generation)及永久死亡(permanent death)的遊戲。
20XX的最終美術畫面,我們重新設計了好幾次。

這文章中,我將說明開發中的重大時間點,並且稍微說明過去這四年我們學到了什麼。

開發真相


20XX是用C++自製引擎製作。我為了履歷而自己製作引擎,本來打算遊戲完成後可以用遊戲開發者的稱號來找工作。我從如何畫出三角形開始做起,20XX也是從這時候開始。我們實在應該直接用Unity的。

產品開發時間軸

第一天: 2013/7/1

Steam Early Access: 2014/11/25(512個開發日)

推出1.0版(離開Early Access) 2017/8/16(1507個開發日)

詳述時間軸

2013/7/1 開始開發20XX

我做了一個非常簡單的20XX雛型,且到reddit(/r/gamedevclassifieds, /r/forhire)去找到遊戲美術,Zach Urtes。我們簽下了6個月的專案合約,準備去完成我們絕對完成不了大架構。他創造出了第一個Rollster角色(看下面的雛型美術)。

我存下了第一個雛型的畫面,還有"ix"的血量條。
ix代表了第9個我們開發的角色模組,但我們在Mighty No. 9出現後,把它改掉了。
紫色的條狀是梯子,這是我們所捨棄的企劃,很可怕的梯子。

2013/8/31 洛克人之父"稻船敬二"發起了Mighty No. 9 Kickstarter

我花了一個星期的時間去觀查他們的Kickstarter及它所帶來的群眾效應,我覺得被完全打敗了。我花了很多時間(我那時候非常沮喪)研究MN9的專案價值,而且我得到2個重點:

1 MN9的盛大迴響證明了傳統洛克人遊戲在市場上仍有需求

2 我們都有著各自的想法,但我們是非常不同的遊戲。我知道嘗試去做一款和洛克人一模一樣的遊戲是不明智的,所以我做了一些基本上的重大變更。加入我最喜歡的roguelikes及重複可玩性,我們給予了20XX隨機生成關卡及永久死亡懲罰。加入我喜觀的各種怪物及莫明奇妙的特色。我們同時設計區域及線上的多人遊玩模式。這些都是MN9沒有的大賣點。

我們幾乎要放棄20XX了。但是我選擇了去相信這世界是足夠容納20XX及Mighty No. 9的。

2014/1/1 Zach的合約超過6個月的預訂週期

我們決定四月去PAX參展,並開始我們的Kickstartar及Greenlight專案,幾乎是同一時間。我為所有我能找到的發行商做介紹,並全部被拒絕了。(我相何他們做了很合理的決定,我們在那時候還不是很好的遊戲。我們用來簡報的雛型也很簡陋)

我開始尋找音效設計師(啊!!我們真的用暫代的音效展示給開發商)。我們第一次出擊並不讓人印象深刻,我們是這樣告訴自己的。

- 我現在還記得我很興奮地送出開發版本,也記得因為對方不想幫20XX做音效所遭受的打擊 -- 因為這是個不好的專案。我很想為自己辯解,但我選擇問他哪裡不好、哪裡我們該改進,並且感謝他給予我們一長串的改進建議。這給了我們很大的改進空間,並讓遊戲變很更好。這教了我如何從批評中取得資訊。這成為20XX開發的基本要素之一。

我們找到了音效設計師Brandon Ellis(Cityfires),他為我們做了20XX中所有驚人的音樂。Brandon 確定了我們的風格,讓遊戲立刻大大地躍進。

2014/4月  我們同時參加PAX East、開始greenlight及Kickstartar專案。我們開始有點厭惡自己了。

Kickstartar是個全職的工作,在第一天我們決定不要一直盯著專案網頁看。若你打算將Kickstartar與展覽結合,在展覽結束後再開始KS並積極地搜集所有展場上對你有興趣的玩家的EMAIL,可以讓你不會忙到崩潰。

- 我們從第一次出展PAX中學到了很多,這些經驗我過去曾經記錄過,過去的我可能比現在的我更能說明這些經驗。

- KS是殘酷且高壓的事情,我們做的是正確的事但沒辦法將所有時間花在其中。事情過後,我們瞭解到專案過早進行了,等視覺做的更好後可以有更好的成果。但在那個時間我們並不知道這些事。我們的回覆也太過於輕浮且不專業,這會傷害到我們自己。

KS的這個月是我人生中最痛苦的一個月。我若沒有強大的耐心及朋友的幫助是撐不過來的,包含了Bianca(早期行銷、社群推廣、注意東方市場、處理KS之外的垃圾事務、和我結婚"這特別重要")、DJ(雖然沒有和我結婚但幫我建立了初期我們所需的網路推廣)、Chase(測試一堆我們早期的糟糕版本、在我做出一堆垃圾時還持續支援我)。
2014/4月的美術畫面

2014/9月 我們與Fire Hose Games合作

我在IGDA 的會議上遇到了Eitan。我們同意他幫助我們讓遊戲變得更專業,令它可以到足夠販賣的程度。這是我們其中做過比較好的決定之一。Eitan有很多我們所缺乏的業界經驗與人脈。

我們將專案的規模放大了一點。取而代之不在11月釋出最終版本,而是決定釋出Early Access版本,且藉由社群的意見建構剩下的部份。

2017/11月 20XX釋出Early Access

第一天我們只賣出了64份20XX。我嚇到了,我們做了一連串的承諾給社群,而且盡了我們最大的力氣去維持。

- 我們承諾在Early Access期間,每兩週更新一次,我們在每二個週三的晚上6:00更新。我們在遊戲內放了個時鐘,清楚地告訴玩家下次更新什麼時候會出。我們很準時依Early Access的計畫更新,大家都很喜歡我們的更新時鐘。這使我們獲得很好的社群印像,而且給我們很穩定、有週期的方式得到我們接下來工作的反饋。

* 千萬不要許下自己做不到的承諾。若你訂下個日期但沒做到,那會比沒有任何承諾還糟糕。如果我沒記錯,在70次的更新內,我們有2次沒有準時達到,一次延遲了1天、一次延遲了1小時。

- 我讀過每篇在Steam社群的文章、每篇在reddit /r/20xxgame 的回覆。(/r/20xx是被廢棄的舊討論區了),及每篇e-mail。我回覆每一個人,而且當社群成長,有一部份玩家會對我提出更多平常的問題。

* 我們的成功一大部份來自於長時間的分析社群反饋。篩去情緒上的字眼再去分析剩下的。當玩家很喜歡你的遊戲時,不會只用客觀的角度提出建議。同時也不要將憤怒玩家的好建議給踢除。

- 我儘量將我們所做的事對社群公開,讓玩家的意見來決定我們的規劃。若大部份的玩家對某Bug十分在意,我們會停下手邊工作來先修復它。若社群認為我們的方向錯了,我們就認真的論是否我們要修正方向。

* 在2015 9月時,我們花了很多時間改造Ace及Nina的角色設計。
上面是20XX重新設計的角色(2015夏季)
下面則是之前的設計

- 新的設計讓我們獲得了前所未見的負評。我們十分沈重的討論,放掉所有我們決定在Beta所做的項目,將心力花在重新打造舊的角色設計(藉由之前錯誤設計的教訓)且剛好完成新的展示影片及宣傳在beta釋出前。
重新設計後的角色們,在推出前又再次改進了一次

- 這次的調整是我們最大幅度的修改我們的計劃。我很確定如果我們堅持修正我們的槍而不去重新設計我們的角色,我們在beta時就會淹死了。

* 對社群開放不表示你要過渡承諾,分享你可以掌控的項目。

2015/3月 了解到我們的遊戲需要提供視覺設計,我們雇用了Clemens Scott

Clemens(最近的作品是Broken Rules工作室的Old Man’s Journey)花了6個月,將我們的遊戲由"沒有吸引力的"變成"極具特色的"(巨大的躍進)。沒有了Clemens的幫忙,20XX宣傳不會如此成功。

2015/4月 我們第二次在PAX展出我們的作品,這次在我們喜愛的Indie MEGABOOTH展出。

這次比我們上次展出好多了,而且我們甚至得到了些宣傳。接下來的一個月,MANvsGAME 及Zeke(Ezekiel_III)開始實況20XX,那時是凌晨兩點,我正在Bianca(女友/現在是未婚妻)的家裡。我一直看到凌晨五點,好險他們沒有把我怎樣。有了Clemens的第一波美術改進很令人興奮,這是很有幫助的合作,我們在隔天就賣出了64套遊戲。而且我簡直如同過節般的快樂。

在9月15號,Mighty No. 9也宣佈了他們的第一次延期。這並不是什麼壞事,延期總是會發生,這時他們也解釋了為什麼會延期、且告訴大家正在努力。

2015/5月 資金快要見底了。Bianca私人大力的贊助我們。她是真的是我們的天使。我去銀行貸款了,所以我可以繼續雇用Zach。

2015/8月 Mighty No. 9第二次延期了。這次他們處理的比較不好(有很多文章講這件事-例如這篇https://kotaku.com/the-story-behind-mighty-no-9s-shady-delay-1722766843)。Mighty No. 9損失了不少支持,我們覺得這是宣傳的好機會。

在MN9宣佈延期不久後,我們發佈了Beta版本。我們受到前所未有的媒體關注,我們由販賣不到多少款變成銷售成績不錯的遊戲。

我們仍然很辛苦的在開發,但從現在開始20XX賺得的錢比開發費用還要多了。蜂擁而來的玩家、收入、與評價完全出乎我們意料之外。之後的一個月,我們保持著穩定的銷售狀況且終於還完了借款。

2016/1月 20XX已經成長了。我們決定在每個更新前都需要社群的意見。

我開啟了我們的"超級好友"群組,為了在上線前的早期開發就能取得玩家的意見。我們盡了最大努力讓早期版本能覺得好玩。上線前看新的物品及關卡是很有趣的,但測試引擎的改版則是不是。

我們提供他們初期的開發關卡,能完成這些關卡真的是需要很多好運。第一版的這些關卡還需要很大部份的改進,而超級好友的意見正是改進的重要來源。

2016/5月 我們決定20XX應該有些過場畫面(所有專業的遊戲都有過場畫面),所以我們開始徵人。

過場畫面真的是件很難做好的工作。我們花超出預算且整個流程花了14個月完成。我們之中沒有人專精於此,所以要討論我們到底要找什麼東西是困難的,但經過了這一切,相較於我們學到了許多東西,我更覺得我們是僥倖存活了下來。

2016/6月 Mighty No.9 PC正式版釋出
玩Mighty No. 9不如玩這個吧.

在MN9釋出之前,20XX最中肯的評論是"如果你不想再等Mighty No. 9,你可以來玩這款"。Mighty No. 9發售之後,雖然它做得還不錯,但仍不如它所宣稱的那麼好。這些評論也改變了,很好運地,MN9釋出的日期正好差不多是Steam的夏日特賣,20XX剛好接收了對MN9失望的玩家。

從此開始20XX的開發變得順利許多。我們維持了一定的銷售量,並在每次特價時期提供20-25%的折扣。直到2017/8月1.0正式版釋出為止都十分順利。

2016/12月 我們發佈了"piecemaker"試驗,讓玩家可以使用我們恐怖的關卡設計工具。

這個遊戲工具糟透了。超過一百位自願實驗者,只有兩位有拿出一點成果,而其中之一將它放入了遊戲。這百分之百是我們的錯 - 我們的關卡設計系統太笨重且很難以上手。

- 做一個更好的編輯器成了1.0之後的問題。我們仍然(至2017/11月為止)在思考這是否為20XX合理的計畫。(如果你很有經驗做遊戲編輯工具快來聯絡我吧!)

2017/8月 過了71個持續性的隔週更新,20XX離開了Early Access,在PC上推出了。

我們做了新的展示影片,但這100%仍然是動作取向。(歡迎大家關於這點提出意見,我們認為這方面還有很大的改進空間)

之後的1個月,我們和幾乎每個對我們遊戲有實況主接觸,並親自寫了信件給媒體。我們並沒有得到很多回應。有3個比較大的媒體會在發行時幫助我們,這是其中一個。(Tim的影片十分強大,即使你不喜歡20XX)。當Kotaku影片發佈時(約遊戲發行後的第3天),我們的銷量比前面的2天暴增了30%。雖然很難指出多少是被影片吸引的(20XX在週末也總是賣得比較好),但這仍然給了我們很大的幫助。

有一大部份來自於我們的發售時在Steam的銷售排行前20-25名。Steam的排名拯救了我們的能見度。雖然我們在媒體上沒有很到很大的曝光,但在Steam的演算法下我們賺到了不少,我們在發佈的首週表現得很有競爭力。

最終產品

未完待續(下篇會歸納從製作中習得的經驗)

2018年1月30日 星期二

[翻譯]專案管理 Part1: 避免遊戲工作破壞腦袋


原文:
http://www.midnighthub.com/blog/2017/11/01/project-management-part-1-avoid-brain-damage-from-working-with-games/



作者:Sara Casen
遊戲開發者,過去在Paradox及Tarsier任職。現在經營Midnight Hub,之前在Casen Crowd,社群經營公司。

為什麼這麼多的遊戲工作室超時工作?如何避免你的組員的熱情燃燒殆盡?以及如何讓你的開發夥伴投注熱情及創意在工作上?及有什麼方法可以取代惡名昭彰的遊戲開發?若你是個管理者從專案開始便將超時工作視為正常,或是認為燃燒熱情本來就是製作遊戲的一部份,那這一系列的開發文章不是為你所寫的。但是你若好奇為何我們花上數個小時來寫這篇文章,你想去最小化你的團隊的壓力、或你想要些遊戲專案管理的秘方,那就讀下去吧。

遊戲製作生涯的平均長度

這個夏天我參加了一個遊戲CEO及管理者的聚會。這個聚會在德國旁的小島上。北歐國家的管理人都在一起討論遊戲產業的現況。在這會議中,大家談論了北歐遊戲製作生涯的平均長度。猜猜看是多長?答案是4年!平均開發者會在4年之後離開遊戲產業。他們可能燒光了熱情、犧牲了健康、或花光了資產。我並沒有確切的數字,但有報導指出這個產業是如何一次次的消耗熱情,使其產業活下去。超時工作被視為常態,一週工作80小時並不少見。但科學證明這並不能讓遊戲變得更好。

在過去百年來的工業研究證明了以上的問題,過渡工作的工人造成更多的錯誤、計劃延宕、損壞工具、多餘的支出、降低貨物的品質、甚至影響收益。他們對於專案、管理、員工、其它人甚至自己都是危險的。
– Evan Robinson, IGDA “Six Lessons On Why Crunch Mode Doesn’t Work”.

在2015年我們開始了自己的工作室 "Midnight Hub",我們想要做些改變。之前我們曾經在Minecraft、The Division及其它專案工作。我們的目標是用穩定的方法持續產出獨特的遊戲。我們是5位獨立開發者,現在花了兩年在第一個遊戲上。"Lake Ridden"。這是個單人第一人稱充滿神祕的解謎遊戲。我們並非總是做對所有事情,但我們仍然努力去嘗試。

這段時間我們儘量不超時工作,我們仍有效利用工作時間且產出許多我們遊戲的文件。我們團隊儘力做出最大效率的產出,並且確實記下每個人的壓力狀況。我將在這分享專案管理的哲學。我們是如何最小化壓力所帶來的影響,並且讓遊戲可以準時在2018春季上市。這包含了許多的層面,所以我將它拆成三篇文章,每篇專注在不同的方向。

提醒一下大家,我們是生活在社會主義的瑞典,這邊一般工作約佔每週40小時,而且員工有權力要求數週的休假及帶薪產假、陪產假。遊戲產業在這邊會更加的扁平化及外國人稱讚我們會主動幫助工作上的夥伴。我們通常也鼓勵彼此做各種嘗試,並且也不認為上司會知道所有的知識。不幸地我們也是會被壓榨的,很多人在遊戲業的工作時數也很長。犧牲假期去追求永遠也無法完成的完工日期。最後迫使他們帶著極大的工作傷害離開遊戲業。

為什麼我們以這種方式工作?


我第一個遇到擁有真實人生的遊戲開發者,是在DICE的Battlefield 1942團隊。他告訴我團隊是如何被消耗到最終失去了他們的熱誠。他們睡在自己辦公桌而不回家,他們的腦袋漸漸地變得緩慢,人們忘了如何去談話、憂鬱、失眠、漸忘。這些都是持續強烈的壓力的徵兆。這些逐漸地傷害你的身體、神經系統、及腦袋。思考看看由製作遊戲造成的腦部傷害。這是有可能被修復的,即使在有社會醫療良好的瑞典,也要花一段很長且辛苦的時間。

在這種工作環境很容易發現一個問題,一些很優秀且具熱情的開發者會不斷的流失。而且當他們開始覺得這是正常的,所以認為忍耐才可以讓事情完成,認為這樣才可以讓遊戲做出來。遊戲當成藝術媒介或故事媒介,這並不是個累積業界經驗的方法。因為有想像力的創作者會在幾年內被榨乾,大多數的人們不會想再次回到遊戲業。那個他們花費許多投資及時間去完成作品的地方,同時也將他們燃燒殆盡。若你也曾經因過渡工作而腸思枯竭,甚至你還需要面對這種症狀數十年。而我們是如何讓遊戲媒體帶邁出這種困境的。

當你擁有創意的人、對工作的熱誠、完美主義者,並將他們混進數千萬預算及糟糕的排程,事情通常會變得很糟糕。人們被強迫超時工作,最終失去他們。不僅是過渡工作同時也無法讓遊戲品質提升。

公司的佈告欄,給予大家分享及鼓勵彼此

為何要用穩定的方式製做遊戲?


所以為什麼我們想要減少壓力、避免超時工作及從第一天就開始防止員工流失?也許是我瘋了。但能成為一個正常的人類感覺很好。若你想用同一批優秀的人才做出更多好遊戲,你該避免讓他們過渡工作。壓力降低了我們的耐心及溝通(你是否看過團隊成員在類似狀況"我不想立刻和你解釋,快去處理它")。遊戲開發是一個團隊運動,若你不打算溝通會造成分崩離析。據我的經驗,壓力是人們開始爭吵的主要因素。

另一方面,人類(抱歉,我遇到某些管理者視人類只是種資源),我的專案管理經驗中,大多份管理者只把人類視為一個可壓迫的變因。若金錢、時間、其它因素是不可變的,那人們就會驅向更加壓迫人類變因。那是因為你很難去衡量能由人類身上取得多少。有的人每週可工作80小時,有些人每天工作6小時後就沒力了。所以管理傾向儘量壓迫人類資源。你知道你在銀行的金錢什麼時候會燒光,但你的員工呢?當你知道迫使人們會失去熱誠,可能已經為時已晚。在我的觀點中,取得金錢要比修復人們的工作傷害要簡單多了。

較短工時的科學根據


有很多研究顯示更多的工作時間並不等於更多的產出。就像是做多件工作比較划算一樣(還需要付出切換工作的成本),讓你感到很有效率但事實不然。若你是在做高腦力的工作,像是編寫程式、寫故事,實際上人腦一天只能夠專注約六小時。過了這段時間,你開始懈怠且開始犯錯(無論如何,這仍然會讓人們感到有效率),然而你必須在隔天整理昨天幹的蠢事。專注於聰明的工作,而不是努力的工作。我相信聰明的管理者該妥善利用各項資源,來做出最佳的遊戲。為什麼不該把這些技能也用在員工身上。

找到新的團隊成員、安置他們、提升效率、確定他們符合團隊等等,是一項艱難的工作。所以照顧員工一直以來都是個好的投資,總比盡力壓榨他們然後再找新的人來取代。老實說這也是因為我們相信一定有更好、更人性的方法來做遊戲,讓開發者有效開發及玩家喜愛。

製作迭代前後比較。迭代是Midnight Hub的主要遊戲製作流程。白色方塊是測試元件,如上圖

事實上大部份的壓榨及超出排程、浪費金錢來自於錯誤的計畫、規模。當然像是引擎意外更新、相同市場上的競爭者、主要成員的離開等重要的因素也是。但我想你若能用乾淨的方法管理你的遊戲專案,會比你增加員工的工時,讓遊戲更有機會準時上市且打造更長遠的團隊。更重要的是,這是你少數你可以真正控制的變因,你無法控制主企劃突然去了地球另一端、或是你的遊戲被破解、或新的遊戲引擎版本讓你的遊戲壞了。但你可以且應該控制你的計劃、遊戲規模、及專案管理,不該有任何藉口。

關於縮減壓力的實際例子


這是我們讓所有人不被壓垮在Midnight Hub做的事。記著若你是領導者或資深開發人員,所有員工都會看著你所做的事。若你不想要讓別人超時工作且親自執行,那所有人會開始真的相信他們不該待在公司太久,言教不如身教。

-減少在公司的時間。我們待在公司9:00-17:00,且有1小時的午餐時間。
-所有人同時間離開公司,沒有人會獨自留下辬公。
-管理必需避免員工超時工作及壓榨。
-所有員工放的到特休
-若你正在處理巨大的任務,可以調整成只工作80%或50%時間。因為那些任務可能會榨乾你的精力。他們可能是你從來沒做過的事,非常困難,要花了數週、數月去完成,而且做錯了會有很嚴重的後果。像是擬定開發合約、對投資人的簡報等。
-規畫時間緩衝放假的心情
-在假期內完全關閉辦公室,不讓任何人單獨工作。
-員工生病假的第一天就可以拿到80%薪水(瑞典通常第一天不支薪)
-在規劃時期給出更佳且更實際的時間估算
-用ABC法則製做遊戲(接下幾篇文章會提到)
-在每週開會評測每人的壓力


整理一下

在Midnight Hub開始時,我寫了有關如何白箱測試遊戲的文章,迭代開發每個部份。我們如何用敏捷開發的方法,將測試用美術轉成可正式美術素材每次一個迭代。這方法讓我們不用花費過多時間就可以有機會測試它,我們現在將其粹取出來。我會在下一章解釋如何用這方法,讓你有機會試用這配方,我們叫它"ABC"。之後我將告訴你,我們如何用實際的金錢去估算時間且規劃我們的迭代。我的目的是分享這方法幫助大家建構長久的團隊,且讀者的回饋也可以幫助改良我們的方法。你在工作上所耗費的心血將不會對你自身造成傷害。

2017年11月10日 星期五

如何避免你的程式大爆炸了?

原文連結

在1960/10/24,蘇聯拜科努爾太空發射場發生了一場大爆炸。在準備飛行的期間,火箭意外爆炸了,造成了火災及無數的破壞。超過70個人在這天死亡。USSR的戰略火箭計劃大大的退步。這場大災難被命名為Nedelin,這個計劃死去的執行負責人。

我們重新檢視造成這場大災難的步驟:

1. 工程師在非常緊湊的排程下工作

2. 因為時間不夠,許多安全程序都被省略或者沒正確執行。

3. 許多意外及缺失在發射準備期間被忽略或沒仔細分析過程

4. 在爆炸當天,為了加速發射流程,許多測試被迫不照順序同時執行。

5. 因為火箭啟動的延遲,工程師沒有正常理由就直接在正式環境作業。

6. 當天有人看到發射按鈕沒在原位而將它擺回原始位置。

7. 其它元件也被放在錯誤的地方,造成系統執行發射命令並且點燃了第二階段引擊,使得沒準備好的火箭爆炸了。

對照遊戲開發,我列出了以下狀況


Initialization, update and deinitialization patterns
Bad input data
Too much control exposed in data
Dereferencing null pointer
Big classes
Division
Vector normalization

同時我也提供了如何防範錯誤的做法及範例。

防衛性程式及合約式設計(Design by Contract)

防衛性程式
不該相信輸入的資料,而是預先檢查資料的正確性並給予安全的數值。
下例:即使不正確的參數,仍然給予安全的答案:零。

float GetSquareRoot(float Argument)
{
    if (Argument <= 0.0f)
    {
        return 0.0f;
    }
    return sqrt(Argument);
}

合約式設計:
設定程式與用戶關聯性
程式必須設定前置條件、後置條件和其不變性。

條件可被下列方式呈現:
assert
comment
etc. (a set of test)

下例:使用註解(comment)及assert設定程式的合約
// Argument needs to be equal to or greater than zero.
float GetSquareRoot(float Argument)
{
assert(Argument >= 0.0f );
return sqrt(Argument);
}


這兩項方法都能幫助我們寫出更良好的程式。
無論如何,我相信遊戲程設要能其特別的狀況而被特別對待。

1. 最大化執行速度是遊戲的重點

防衛性程式無法用在程式的每個地方,因為這會拖慢程式速度,且額外的程式會造成其它錯誤。我們的遊戲就遭遇過執行速度的問題。同時太多的防衛性程式使錯誤不容易發現(被防衛性程式擋下)。

2. 程式在變更迅速的環境下被完成。

遊戲程設包含了快速開發、創意驗証。但是因為要求速度會造成未來維護的問題。

3. 程式必須同時能用於產品及測開發階段

當我們在製做新的功能時,編輯器的新工具或改良也在同時進行,我們的第一個使用者是其它團隊成員、設計師、美術人員。他們也在製作遊戲及使用我們的工具。這表示了我們的程式同時被開發團隊及消費者所使用,即使在釋出給玩家之前。用assert強制程式對於工程師是好方法,但是同時會中斷其它成員的工作流程。

另外你要面對的問題可能和你遊戲、團隊、工作模式有關。Rendering的工作就會以速度為優先考量,而AI的工作就會需要更多穩定的程式。

我們將會介紹遊戲開發會遇到的常見幾種狀況。

Initialization, update and deinitialization patterns

讓我們看這個RAII(Resource Acquisition Is Initialization)的實作
class MyClass
{
public:
    MyClass()
    {
        m_Data1 = new DataClass1;
        m_Data2 = new DataClass2;
    }
    ~MyClass()
    {
        delete m_Data1;
        delete m_Data2;
    }

    void Update(float FrameTime)
    {
        m_Data1->Update(FrameTime);
        m_Data2->Update(FrameTime);
    }

private:
    DataClass1* m_Data1;
    DataClass2* m_Data2;
};
我們在constructor創造2個物件,然後在destructor刪除它們。並且包含了一個常見的Update函式,會在每個frame被呼叫一次。

現在讓我們把記憶體管理移出constructor、destructor,並且創造一個函式來控制這些資源。這是為了避免在同一個地方執行太多的操作。通常資源管理是個佔效能的操作,所以我們會希望在遊戲中能方便的控制它們。我們創造2個函式Initialize and Deinitialize來管理記憶體。

這是新的程式碼:


class MyClass
{
public:
    MyClass() { }
    ~MyClass() { }

    void Initialize()
    {
        m_Data1 = new DataClass1;
        m_Data2 = new DataClass2;
    }

    void Deinitialize()
    {
        delete m_Data1;
        delete m_Data2;
    }

    void Update(float FrameTime)
    {
        m_Data1->Update(FrameTime);
        m_Data2->Update(FrameTime);
    }

private:
    DataClass1* m_Data1;
    DataClass2* m_Data2;
};

這程式仍然十分單純,但這有許多的問題。

1 這沒有適當的initialization在constructor中
2 如果Initialize被呼叫了兩次會發生什麼事?這有可能造成memory leak
3 在Deinitialize也有可能會發生相似的問題
4 另外個問題關於Update,若沒有先呼叫Initialize而直接執行Update會發生什麼事情?會因空指標或被初始化物件造成crash.
5 若複製這個物件,有可能造成多次刪除的問題。

我們這裡沒有特別做合約式設計,我們只是考慮這個類別有可能的使用狀況。

為了使此程式穩定、減少出錯。我們需要

1 提供適當的initialization給我們的成員變數
2 加上bool參數m_Initialized 使memory leak、及多次刪除不會發生。
3 正常狀況下,若指標已被釋放,我們應將指標位置清除
4 由destructor呼叫Deinitialize,避免使用者忘記呼叫。
5 定義private copy constructor and copy operator, 防止它們被複製。

更改後如下:

class MyClass
{
public:
    MyClass();
    ~MyClass();

    void Initialize();
    void Deinitialize();

    void Update(float FrameTime);

private:
    MyClass(const MyClass&);
    MyClass& operator=(const MyClass&);

    bool m_Initialized;
    DataClass1* m_Data1;
    DataClass2* m_Data2;
};

MyClass::MyClass()
    : m_Initialized(false)
    , m_Data1(nullptr)
    , m_Data2(nullptr)
{
}

MyClass::~MyClass()
{
    //Prevent a memory leak if the client 

    //does not call Deinitialize.
    Deinitialize();
}

void MyClass::Initialize()
{
    if (m_Initialized)
        return;

    m_Data1 = new DataClass1;
    m_Data2 = new DataClass2;

    m_Initialized = true;
}

void MyClass::Deinitialize()
{
    if (!m_Initialized)
        return;

    delete m_Data1;
    m_Data1 = nullptr;

    delete m_Data2;
    m_Data2 = nullptr;

    m_Initialized = false;
}

void MyClass::Update(float FrameTime)
{
    if (!m_Initialized)
        return;

    // Defensive approach to check the pointers separately.
    if (!m_Data1 || !m_Data2)
        return;

    m_Data1->Update(FrameTime);
    m_Data2->Update(FrameTime);
}

Bad input data
(壞掉的輸入資料)

壞掉的輸入資料從資訊產業誕生的那一天就有了。檔案遺失或例外參數都會造成遊戲或編輯器當掉。

通常工程師會特別注意這問題。我們的程式需要為了參數遺失,特別寫預設的參數,像是預設的貼圖或可視的物件來提醒輸入錯誤,給予錯誤訊息及記錄也是有效的方法。沒有程式應該直接掛點。

在多數的遊戲引擊,我們可以很輕易地給予編輯器參數。讓我們可以利用這參數在一行指令就生成數個物件。

int NumberOfProjectilesToSpawn;

通常我們只會給他0,1,5這種數量。但有時使用者會犯錯或給與一個超大的數值像是100萬、或負數?那時候我們的程式碼還可以正常運作嗎?

好的方法是去考量現況決定參數的預設值及範圍。另外一種方案,如果我們想要不被限制的超大參數,我們提醒使用者輸入的參數可能會造成問題。

在防衛性程式,我們可以很清楚的區分有危險的參數。我們將所有input視為不可信的,但有些被驗證的內部資料是可以相信的。例如,我們認定所有外部檔案、編輯器輸入參數、玩家輸入參數、網路上參數都是有不可信的。驗證及修正函式需要將這些參數轉成可信的參數。

void SetNumberOfProjectilesToSpawn(int NumberOfProjectilesToSpawn)
{
    m_NumberOfProjectilesToSpawn = NumberOfProjectilesToSpawn;
    ValidateAndCorrectParameters();
}

void ValidateAndCorrectParameters()
{
    m_NumberOfProjectilesToSpawn = clamp(m_NumberOfProjectilesToSpawn, 0, 8);
}

在遊戲開發中,越早驗證是越好。例如:在編輯器輸入參數時就驗證,在存成檔案之前,或是在檔案一讀進來。越早確認使其更容易減少執行遊戲不必要的測試及效能衝擊。

Too much control exposed in data
(太多對資料操作的方式)

由壞掉的輸入資料所造成的問題再更進一步思考,我們所有的系統都是資料導向的。這些系統能被設定或在沒有工程師的狀況下被操作,完全透過所輸入的資料。

通常會將所有操作及遊戲特色給每個組員,尤其是非工程師。但是工程師必須問問自己,我們真的可以提供所有組合參數嗎?或者反而造成更多例外狀況?

創造一個真正資料導向的系統是非常困難及花時間的。

當我們提供任一個參數,這都會成為整個巨大資料導向系統的一部份。很自然的,我們會加入一個介於程式與資料之間中間層。

讓我們看這用來控制敵人的簡單結構

struct EnemyBehavior
{
    float m_WalkSpeed;
    float m_TurningRate;
    bool m_JumpAllowed;
    float m_JumpSpeed;
    // …
};

這個程式提供m_WalkSpeed 的小參數,也同時提供m_TurningRate 大參數。當m_WalkSpeed 很大時跳躍的功能仍然可以正常運作嗎?

一個較安全的方案是準備一組預先設定好的參數組,而這些參數都是可正常執行的。使用者只允許使用這組先定好的參數。

enum EnemyBehaviorPreset
{
    EnemyBehaviorPreset_TightCorridors,
    EnemyBehaviorPreset_IndoorHalls,
    EnemyBehaviorPreset_OutdoorOpenSpace
};

我們只提供一個參數讓使用都可以在編輯器使用。

EnemyBehaviorPreset m_EnemyBehavior;

這個方法雖然限制使用者的自由,但是對工程師有更佳的穩定及維護性。

Dereferencing null pointer
釋放空指標

Dereferencing null pointer是造成遊戲或編輯器當掉的一項常見因素。

在Unreal Engine 2 and 3,其中一項程式語言UnrealScript的設計目標就是防止這類問題。並且創造不需指標的開發環境。存取物件references在UnrealScript一直是安全的,即使它們沒有指向任何物件。

因為執行速度是很重要的,所以我們不能在所有釋放指標的地方做檢查。無論如何每個部門處理指標的方針也許不一樣。繪圖工程師會犧牲防衛機制去換取速度,但是AI或gameplay則會希望更多的防衛機制,尤於gameplay時常更動。所以如何處理指標會按其環境而定。

-Avoiding pointers(避免使用指標)

第一、我們可以避免使用指標當函式的參數且用C++參照(references)取代,下例即是說明如何取代指標

void MyFunction(ControlData* InputData)
{
    if (InputData)
    {
        int parameter1 = InputData->GetParameter1();
        // …
    }
}

改成用reference的版本

void MyFunction(ControlData& InputData)
{
    int parameter1 = InputData.GetParameter1();
    // …
}

這樣使用者在用這個函式時就會提供適合的參數,並確認及釋放指標。

-Public and private methods(公開及私有函式)

實際上,有項不好的慣例是在公開的函式傳入的指標當作參數,

若這函式是屬於公開介面的話,我建議要檢查這函式是否為空。

void MyClass::AnalyzeInputData(Data* Input)
{
    if (!Input)
        return;
    // …
}

若函式是屬於私有實作的一部份,我會傾向將先前的程式移除。在這例子,函式有註解標明、或是函式會發出警告訊息去確認指標狀況。

class MyClass
{
public:
    // Method always tests if the pointer argument is null.
    void AnalyzeInputData(Data* Input);

private:
    void UpdateObject_NoTestForZeroPointer(ObjectClass* Object);

    // Method does not check validity of pointer arguments:
    void UpdateObject(ObjectClassA* Object1, ObjectClassB* Object2);
};

另外在私有函式UpdateObject及UpdateObject_NoTestForZeroPointer的開頭,我們可以利用assert驗證指標

// Method does not check validity of pointer arguments:
void MyClass::UpdateObject(ObjectClassA* Object1, ObjectClassB* Object2)
{
    assert(Object1);
    assert(Object2);
    // …
}

無論如何這不是最實際的做法,若我們的assert中斷成員在使用編輯器。要改進這個方法,我們可以創造一個防禦性軟驗證。

-“Soft” assertions

軟驗證應該允許程式繼續執行,但仍會回報問題給開發者

void MyClass::UpdateObject_NoTestForZeroPointer(ObjectClass* Object)
{
#ifdef CONTRACT_SAFE_CHECK
    reportIfNull(Object); // Soft assert
    if (!Object)
        return;
#endif
    // …
}

這方法通知程式人員不正常的狀況(將訊息送到資料庫)且不會中斷執行。利用#ifdef設定是否要執行這項檢查。開發設定應效能分析(profiling)及正式環境可執行。

編譯前的參數也要考慮到是否在編輯器執行或是遊戲內會造成其它問題。

Big classes

有些class成長速度太快了。所以有些原始檔會變得比其它檔案還大。若你去檢查你正在執行的專案,你會發現有些檔案很明顯地較其它檔案大上不少。這些檔案通常是負責角色、玩家或是遊戲管理者(manager)。這並不令人驚訝。當我們增加新的特色進遊戲時,即使是最小的步驟,我們也會將它加入現有的class底下。這種增加速度是會造成問題的。這使得我們很難去維護preconditions, postconditions and invariants。維護大型的類別是很困難的,且他們也會造成更多問題。從Initialization, update and deinitialization patterns要發現問題會比較簡單。通常他們包含了許多沒有分類好的子元素。這有許多可能的方法可以解決這個問題。我在這展示其中一個方法。

若我需要在現有的class中加入一項新的元素,我開始評估是否該為其建立nested class。下例在ObjectBehavior 內新增成員參數及函數去為了完成Special Action 1。

class ObjectBehavior
{
public:
    // …
private:
    // …
    // Begin Special Action 1.
    bool m_ShouldStartSpecialAction1;
    float m_TimeLeftToStartSpecialAction1;
    float m_SpecialAction1Parameter;
    void InitializeSpecialAction1();
    void DeinitializeSpecialAction1();
    void UpdateSpecialAction1();
    // End Special Action 1.
    // …
};

以nested class方式實作:
以子類別
class ObjectBehavior
{
public:
    // …
private:
    // …
    class SpecialAction1
    {
public:
        void Initialize();
        void Deinitialize();
        void Update(ObjectBehavior* ParentObject);
    private:
        bool m_ShouldStart;
        float m_TimeLeftToStart;
        float m_Parameter;
    };

    SpecialAction1 m_SpecialAction1;
    // …
};

哪一個方法比較好?我們適度的包裝了新的參數在獨立的私有類別中。只有SpecialAction1 nested class被存取到。這使得類別在未來可以更容易被區分出來。

除法(Division

電玩遊戲用了大量的浮點數運算。例如,我們經常使用除法在我們的程式內。這是非常基本的數學運算,但這仍然要注意不要讓除數為0。若我們除數為0時,會造成浮點數的例外狀況,或者浮點數的例外被擋掉了,算出來的結果會是無限(INF)或非數字(NaN)。

請也考慮到下面的狀況。當兩個數字都是合法的浮點數,但是結果會是INF,因為他們超過32-bit浮點數可表現範圍。

float f1 = 100000000000000000000.0f; // 1.0e20f
float f2 = 0.0000000000000000001f; // 1.0e-19f
float f3 = f1 / f2;

f3即為INF。另一個例子:

float f4 = 0.0f;
float f5 = 0.0f;
float f6 = f4 / f5;

參數f6是NaN

我們可以繼續執行程式,因為INF是個合法且可計算的特別記號。無論如何,在多數狀況下,我們不會想去處理這種狀況。許多函式庫會因這些例外狀況中斷。

Unreal Engine 4 提供了時別的函式去偵測浮點數太接近零。

bool FMath::IsNearlyZero(float Value, float ErrorTolerance = SMALL_NUMBER);

在 C# Unity Engine可以被寫成這樣:

bool IsNearlyZero(float Value)
{
    return Mathf.Abs( Value ) <= 0.00000001f;
}

我們可以在做除法之前先確認我們的除數。

float denominator = b – a + c;
if (!IsNearlyZero(denominator))
{
    result = d / denominator;
}

要確認特別的浮點數,我們擁有以下函式:

float.IsInfinity(float) and float.IsNaN(float) in Unity Engine
FMath::IsFinite(float) in Unreal Engine 4

同時我們經常忘記某些特別狀況會用到除法。請考慮以下的update函數。

void Object::UpdateLogic(float FrameTime)
{
    Vector3 Velocity = (GetCurrentPosition() – m_PreviousPosition) / FrameTime;
    // …
}

我們可以很簡單的假設FrameTime總是大於0。當然程式會在FrameTime為零時出錯。這是有機會發生的,例如當遊戲在製作時我們相要將時間暫停,但是update仍然被呼叫。在編輯器,我們通常不會更新world logic,但在某些特別的狀況,我們會為了模擬而更某些物件的邏輯。這簡單的程式會因為這些特例而掛掉。

Vector normalization

在Vector normalization會出現某些例子上。通常是計算3d數學時。normalization是為了計算出單位向量的方法,會將其向量除以自身長度。這些函式都非常單純,我們再次回想起了除法的例外。當向量為零時或非常接近零時會出錯。

在Unreal Engine 4,我們有以下兩個函式:

FVector::GetUnsafeNormal() – 這是不會做額外檢查的normalization,但比較快速。

FVector::GetSafeNormal() – 這是會檢查除數是否為零,若長度接近零時,結果回傳零。

有趣的是它用很直接的寫在函式名稱上提醒使用者。

Unity Engine在C#則是只有提供安全的normalization

Vector3.normalized

Vector3.Normalize()

所以它會回傳單位向量或是零向量。

“Soft” methods

我同時也會提供些通用的方式來提升程式品質。

測試你自己的程式碼:
自動化測試、
視覺化除錯及文字記錄、
採用有意義的命名、
程式檢閱(code review)、
靜態程設分析工具、
程式文件

總結:

在這文章中,我展示了遊戲程式設計上常見的錯誤及一般問題的來源。我們知道追求安全及穩定的工程方法並非新的挑戰。這也不是簡單的工作,尤其在特殊的狀況下,像是遊戲開發。主要困難來自於去知道、預測及決定什麼地方該用防禦性程式及該用多少的安全措施才足夠。

將高安全性軟體工程的做法完全放到遊戲開發是不明智的。例如:太空計劃。無論如何,這可以用來了解人們在這些產業是如何完成他們的目標。

NASA太空中心主管及Saturn V rocket首席工程師,特別喜歡實作大型及安全的軟體工程。這些通常會在開始時花不少時間,但最終帶領美國完成了阿波羅11的成功。第一個上太空的人也安全的回來了。Saturn V 也是唯一帶著人類離開地球的火箭。