Framer Studio 適合做什麼?

Framer Studio 好像是針對一個頁面的細膩動畫效果,而不能實現多個頁面之間的跳轉邏輯?可以完成整個APP的原型表達么?


確實如答主所說,如果頁面太多可能 Hype 這樣的應用會比 Framer 更適合做原型,但是最新版的 Framer 發布後在處理「多頁面」跳轉方面已經大大的改善,可以有效的利用 Design 里的畫板來做不同頁面之間的跳轉。

另外不能忽略的是 Framer 是最接近編程的原型工具,依託 web 有大量的第三方庫,它幾乎能做到和真正的 APP 一樣強大。舉幾個例子你能用關於 three.js 的庫來做各種3D 模型和交互,能用關於 Firebase 的庫來實現數據的在線存儲和讀寫,能使用關於 MapBox 的庫來導入實時地圖,還有很多很強大的庫,並且隨著 Framer 的流行可能還會更多。

如果你有一些很有意思的想法,拿 Framer 做一個簡單的 Demo 所需要的工作量會比你真正編程要少的多,並且效果幾乎相同。

分隔線下面是我最近用 Framer 做了一個飛機大戰的小遊戲的製作過程,遊戲實現了傷害判定、遊戲難度增加、多屬性敵人、玩家遊戲數據存儲等功能,希望具體的例子對你會更有參考。

-----------------------------------------------------------------------------------

在線試玩和源碼下載:飛機大戰-進階版 (注意會有聲音播放)

為什麼是 Framer ?

一直以來高保真可交互原型工具主要分為兩大陣營,以「 Page」為基礎的輕量化工具如:Hype (它其實不輕,而且到高階必然需要寫 js )、Principle、Flinto ,以「Patch」 為基礎的 Origami Studio 、 Form 。

Framer 在其中應該算一個異類,它要解決的其實和 Patch 類原型工具是一樣的問題:如何處理邏輯比較複雜的非線性交互原型?Origami Studio 和 Form 給出的答案是類似電子元件有輸入輸出介面的 Patch,而 Framer 的答案是「代碼」。

筆者在這裡其實很難苟同兩者各有優勢,拿 Origami Studio 來說,在處理複雜交互邏輯的前提下必然會連出可讀性較差的 Patch 圖。

https://img.zl-hj.com/images/2017/6/4/upload/577344a40cf26eb8c9cb7d1d_ec9a85c5-cd0b-411c-ae62-e34c17962298.jpg/s150070Origami Studio 的 Patch 連線圖

而需要寫代碼的 Framer 雖然一開始學習曲線仍然比較陡峭,但代碼對處理複雜的邏輯和生成重複性的元素具有天然的優勢,並且長久以來程序猿們也總結出了各種增加邏輯可讀性的書寫代碼方法。

Framer 基礎

這部分的內容新手可以參考官方的最新教程:Framer - Basics,只要了解到最重要的 Layer、State 、Animation 即可。然後可以適當了解下關於程序邏輯的這部分內容:Framer - Programming 和一些 Framer 自帶的函數:Framer - Docs。

遊戲的基本規則和目標

這是一款十分簡單的遊戲,玩家用手指拖拽一架戰機,戰機會自己發射激光子彈射向不斷朝你飛來的隕石,子彈射中隕石後會爆炸得分,相反隕石砸中戰機,則生命值減少,總共有三段生命值。伴隨你的得分增加,隕石會越來越多,同時子彈射速也會增加,最終目標是在生命結束前獲得更多的得分。

開始搭建遊戲初始化頁面

首先初始化頁面包含的元素有:

  • 背景
  • 遊戲標題
  • 生命值
  • 遊戲得分
  • 戰機
  • 開始按鈕

1. 背景,選擇一下 Framer 的默認交互設備為 iPhone7, 同時設置一下背景顏色即可。

bg = new BackgroundLayer
backgroundColor: "rgba(0,17,41,1)"

2. 遊戲標題和開始按鈕,這裡放到一起說的原因是標題和開始按鈕在遊戲開始後都需要消失,因此都會有一個不透明度變為0的「State」。在書寫圖層尺寸和位置的代碼時應該考慮不同設備尺寸的適配,通過使用 midX、midY和獲取當前屏幕尺寸 Screen.width、Screen.height 等方法來達到自適應的目的。

#初始化標題
title = new Layer
height: 107
image: "images/TITLE.png"
width: 601
midX: Screen.width/2
y: 289

#標題的「hide」狀態,透明度變為 0
title.states.hide =
opacity: 0
animationOptions:
time: 0.5

#初始化開始按鈕
startButton = new Layer
height: 106
midX: Screen.width/2
y: Screen.height - 250
width: 370
image: "images/START.png"

#開始按鈕的「hide」狀態,透明度變為 0
startButton.states.hide =
opacity: 0
animationOptions:
time: 0.5

如果僅僅是透明度變為 0 ,在開始按鈕被寫上 onClick 方法後還是能被觸發的,因此開始按鈕在完成轉變為「hide」狀態後需要被抹除,使用.destory()方法。

#當開始按鈕轉變為「hide」狀態後,把它真正的抹除掉
startButton.onStateSwitchEnd -&>
startButton.destroy()

3. 得分,需要用到「TextLayer」可以參考這部分內容:Framer - Docs 其 text 屬性是字元串類型,它的值就是要顯示的分數,我們先初始化為 「0」。這個值在遊戲開始後隨著得分增加而變化,因此需要在字元串中使用嵌入表示法#{}來引用其他變數,這個方法會在後面用到。

coin = new TextLayer
height: 54
image: "images/coint.png"
width: 140
x: Screen.width - 160
y: 19
text: "0"
fontSize: 50
lineHeight: 1
textAlign: "center"
paddingLeft: 50
color: "#ffffff"

4. 生命值,為了代碼書寫簡單我用了一種較笨的方法,三張圖片依次顯示剩3段生命值、剩2段生命值、剩1段生命值的情況,最開始是3段生命值的圖片。

https://img.zl-hj.com/images/2017/6/4/upload/577344a40cf26eb8c9cb7d1d_f839452c-236f-43d0-8ff4-a4046bc3fb3f.jpg/s150070生命值的三種狀態

health = new Layer
height: 62
image: "images/health-3.png"
width: 255
x: 20
y: 10

5. 戰機,設置它的拖拽速度,以及拖拽範圍,這部分內容可以參考Framer - Docs,同時為了表現在被隕石擊中後短暫的紅血狀態,使用了兩張戰機圖片來生成兩種狀態default 和 damage。

https://img.zl-hj.com/images/2017/6/4/upload/577344a40cf26eb8c9cb7d1d_f1244b57-b367-406b-afbb-4903496e3ec3.jpg/s150070

# 戰機初始化
plane = new Layer
borderRadius: 4
image: "images/plane.png"
y: Align.center
height: 302
width: 173
midX: Screen.width/2

#設置戰機的拖拽屬性
plane.draggable = true
plane.draggable.speedX = 1
plane.draggable.speedY = 1
plane.draggable.constraints = Screen.size
plane.draggable.overdrag = false

#設置戰機的兩種狀態和狀態轉換的動畫時間
plane.states.default =
image: "images/plane.png"
plane.states.damage =
image: "images/plane-damage.png"
plane.states.animationOptions = time: 0.2

6. 點擊開始按鈕,由於目前還沒有寫遊戲的其他邏輯,開始按鈕只觸發標題和開始按鈕的消失狀態。以後會添加其他需要被觸發的方法如:子彈的發射、隕石的掉落等。

startButton.onClick -&>
startButton.animate("hide")
title.animate("hide")

最後初始化界面完成後,就會得到類似下圖的樣子:

https://img.zl-hj.com/images/2017/6/4/upload/577344a40cf26eb8c9cb7d1d_7a32543a-3196-436c-af3f-5a8b3b88d6e5.jpg/s150070遊戲開始界面

讓戰機發射子彈

這裡的子彈為了簡單方便,設計成藍色激光,用shadowBlur 等屬性來表現子彈的光芒,同時創建一個 Animation 是子彈朝屏幕的上邊緣飛去,通過 midX = plane.midX 來保證每一發子彈的起止發射點都是戰機的中心,最後當飛出到屏幕上邊緣後便被抹除掉,不然會影響遊戲性能。

另外需要設置子彈的父類為bg(就是開始初始化頁面的那個背景圖層),這樣做的好處在於生成了一個擁有所有 bg 子類的數組,我們可以方便的用 for in bg.children 來遍歷所有的子彈。同時為了不斷的生成子彈把它們都寫在一個函數方法中方便調用,將這個函數命名為 shootFunction。

shootFunction = -&>
bullet = new Layer
width: 8
height: 80
midX: plane.midX
y: plane.y
backgroundColor: "rgba(33,196,255,1)"
borderRadius: 30
parent: bg
shadowSpread: 2
shadowColor: "rgba(79,242,255,0.6)"
shadowBlur: 15
shoot = new Animation
layer: bullet
properties:
y: - Screen.height - bullet.height
time: 1.7
curve:Bezier.easeOut
shoot.start()
shoot.onAnimationEnd -&> bullet.destroy()

如果你運行這個函數一次你會發現,它只能生成一發子彈,而在遊戲開始後,我們需要戰機不斷的發射出子彈,這裡就需要用到 Framer 另一個自帶的方法 Utils.interval x ,-&> shootFunction() ,它可以設置每 x 秒執行一次 shootFunction 這個函數,就等價於每 x 秒會發射出一發子彈,關於這個方法可參考:Framer - Docs,同樣為了方面以後調用,我們把這個方法一起寫到一個函數中,同時將這個間隔時間作為函數的參數。

intervalShoot = (shootSec) -&>
Utils.interval shootSec ,-&>
shootFunction()

將這個 intervalShoot 方法添加到之前開始按鈕的點擊事件中,把參數設為0.5即每次子彈發射間隔時間為0.5秒。

startButton.onClick -&>
startButton.animate("hide")
title.animate("hide")
#新增到下方
intervalShoot(0.5)

點擊開始遊戲效果

https://img.zl-hj.com/images/2017/6/4/upload/577344a40cf26eb8c9cb7d1d_6e34021d-a3bb-4dd4-b9ae-4fd978a81db1.jpg/s150070子彈發射效果

隕石的下落和傷害判定

這裡的下落和子彈的發射其實很類似,不同的是需要用到一個 Framer 自帶的 Utils.randomNumber() 隨機數方法來生成水平位置不同的隕石,並且需要一個隕石自轉的動畫,隨機數方法可參考:Framer - Docs。同樣把它們都寫進一個函數中,函數命名為rockNormalFunction

rockNormalFunction = -&>
enemy = new Layer
width: 100
height: 100
x: Utils.randomNumber(0,Screen.width - 100)
y: -100
index: 500
image: "images/rock-normal.png"
rotate = new Animation
layer:enemy
properties:
rotation: 900
time: 3
curve: Bezier.easeIn
move = new Animation
layer:enemy
properties:
y: Align.bottom
time: 3
curve: Bezier.easeIn
move.start()
rotate.start()
move.onAnimationEnd -&> enemy.destroy()

關於傷害的判定,這裡包含兩鍾判定,第一種是隕石是否砸中戰機,另一種是子彈是否擊中隕石,相應的如果是隕石擊中戰機,那麼戰機的生命值就要減少,如果是子彈擊中隕石,那麼遊戲得分應該增加。在這裡我們新建一個完全透明對遊戲無干擾的圖層,用這個圖層的 x 和 y 來分別代表生命值和得分。

#healthAndCoins的x代表生命值,y代表得分
healthAndCoins = new Layer
opacity: 0
height: 1
width: 1
x: 3
y: 0

這樣做的好處在於增加了代碼的耦合性,舉個例子就是我可以在隕石砸中飛機的方法中簡單的寫 healthAndCoins.x -= 1 來代表生命值減少,同時在 healthAndCoins.on "change:x" 方法中寫生命值減少後會觸發那些變化。 而不需要把這些方法都寫在隕石砸中飛機的方法中。另一個原因是 Framer 的 on change 方法不能監聽普通變數的變化,用隱形圖層的 x 和 y 來表示也是一種妥協。

接下來在 rockNormalFunction 中繼續寫上隕石砸中飛機和隕石被子彈擊中的判定方法,使用邏輯方法 if 來進行判斷,通過 for bullet in bg.children 來遍歷到所有子彈的位置,可以參考這部分內容:Framer - Programming。在子彈接觸到隕石後會先開始一個爆炸動畫然後才會消失,同時遊戲得分增加即 healthAndCoins.y += 1。在隕石砸中飛機後,生命值減少即 healthAndCoins.x -= 1 ,同時隕石被抹除。

#寫在 rockNormalFunction 函數中
enemy.on "change:y", -&>
for bullet in bg.children
if (bullet.midX &> enemy.x bullet.midX &< enemy.maxX) (bullet.midY &> enemy.y bullet.midY &< enemy.maxY) (enemy.y &> 0)
explode1 = new Animation
layer: bullet
properties:
scale: 10
opacity: 0.5
backgroundColor:"#FFCB14"
shadowSpread: 10
shadowColor: "#FF9100"
shadowBlur: 10
blur: 2
time: 0.07
animationOptions:
curve: Bezier.easeInOut
explode2 = new Animation
layer: bullet
properties:
height: 8
time: 0.00001
explode1.onAnimationStart -&>
explode2.start()
explode1.onAnimationEnd -&> bullet.destroy()
explode1.start()
healthAndCoins.y += 1
enemy.destroy()
if (enemy.maxX &> plane.x enemy.x &< plane.maxX) (enemy.midY &> plane.y enemy.y &< plane.midY) if healthAndCoins.x &> 0
healthAndCoins.x -= 1
enemy.destroy()

同樣的,為了不停的有隕石出現,需要寫上 Utils.interval 函數來調用rockNormalFunction

intervalRock = (rockSec) -&>
Utils.interval rockSec ,-&>
rockNormalFunction()

將這個 intervalRock 方法添加到之前開始按鈕的點擊事件中,把參數設為1,每次隕石降落間隔時間為1秒。

startButton.onClick -&>
startButton.animate("hide")
title.animate("hide")
intervalShoot(0.5)
#新增到下方
intervalRock(1)

點擊開始遊戲效果

https://img.zl-hj.com/images/2017/6/4/upload/577344a40cf26eb8c9cb7d1d_49f2a602-5bbc-40a8-aedb-1ff7e440885e.jpg/s150070隕石降落效果

生命值變化和死亡頁面

上面已經提到通過 healthAndCoins.on "change:x" 方法可以監聽生命值的變化,一旦變化就會觸發戰機紅血狀態,短暫的紅血狀態結束後會恢復到普通狀態。同時 health 圖層的 image 屬性修改為對應的圖片地址, image 屬性的值使用嵌入表示法#{}來生成不同的圖片地址,圖片名提前命名為有規律的health-1、health-2、health-3 。

healthAndCoins.on "change:x", -&>
plane.animate("damage")
Utils.delay 0.15,-&>
plane.animate("default")
if healthAndCoins.x &> 0
health.image = "images/health-#{healthAndCoins.x}.png"

當生命值等於0即 healthAndCoins.x == 0 時即觸發死亡頁面,死亡頁面顯示你的最終遊戲得分,並且有一個重新開始按鈕點擊可以重置遊戲,重置方法為 location.reload() ,這裡比較有趣的是 Framer 並沒有在官方文檔中提供此方法來刷新頁面,但只要你知道這個方法在 JS 語法中是有效的,那麼用在 Framer 中通常可行,很多類似的方法 Framer 官方的文檔都沒有提到,需要多去嘗試。

死亡頁面出現的代碼也要寫在 healthAndCoins.on "change:x" 方法中,在上面代碼的下方。

else if healthAndCoins.x &< 1 deadScreen = new Layer image: "images/dead-screen.png" width: Screen.width height: Screen.height index: 1000 opacity: 0 showDead = new Animation layer: deadScreen properties: opacity: 1 time: 1 showDead.start() restartButton = new TextLayer width: Screen.width * 0.8 textAlign: "center" height: Screen.height/2 * 0.15 y: Screen.height - Screen.height/2 * 0.15 - 50 midX: Screen.midX borderRadius: 8 backgroundColor: "rgba(255,187,75,1)" fontSize: Screen.height * 0.15 / 4 lineHeight: 2 color: "#792809" text: "Restart" fontWeight: 800 opacity: 1 parent: deadScreen finalNumber = healthAndCoins.y coinDead = new TextLayer height: 54 image: "images/coint.png" width: 147 midX: Screen.midX scale: Screen.width/750 * 1.5 y: Screen.width/750 * 700 text: "#{finalNumber}" fontSize: 50 lineHeight: 1 paddingLeft: 40 textAlign: "center" color: "#ffffff" restartButton.onClick -&>
location.reload()

https://img.zl-hj.com/images/2017/6/4/upload/577344a40cf26eb8c9cb7d1d_4103d14a-4977-4bad-9e6f-ec0e137b7d91.jpg/s150070死亡頁面效果

遊戲得分的變化和難度的增加

和上面生命值的變化相同,使用 healthAndCoins.on "change: y" 來監聽healthAndCoins.y 的變化,並實時更新到 coin 圖層 text 的值中。

healthAndCoins.on "change:y",-&>
coin.text = "#{healthAndCoins.y}"

在得分變化到某些特定的值後(這裡定義的是10乘2的整數次方),遊戲難度就會提升,intervalRock() 方法和 intervalShoot() 方法會再次被觸發來增加隕石數量和子彈射速,子彈射速增加到一定程度後會停止增加,而隕石的數量會隨著得分的不斷增加而一直增加。把這些寫到同一個函數方法中命名為uprageGame()

level = 1
uprageGame = (uprageLevel) -&>
if (healthAndCoins.y &> 10*(Math.pow(2,level))) (healthAndCoins.y &< 10*(Math.pow(2,level+1))) (level == uprageLevel) if level &< 3 intervalShoot(0.5) intervalRock(1) level += 1

在 healthAndCoins.on "change: y" 方法中循環uprageGame() 100次,來應對比較極端的情況。

healthAndCoins.on "change:y", -&>
coin.text = "#{healthAndCoins.y}"
#新增到下方
for i in [1..100]
uprageGame(i)

進行到這裡一個簡單的飛機大戰遊戲就完成了,試玩或下載源代碼 飛機大戰-基礎版

如何增加可玩性?

1. 增加敵人的豐富性,單一的隕石很容易讓遊戲變得無聊,可以考慮三種不同的隕石來增加遊戲的可玩性。

https://img.zl-hj.com/images/2017/6/4/upload/577344a40cf26eb8c9cb7d1d_55ee5dcf-dec6-47ed-b294-283572dbfc0e.jpg/s150070

其中左一是最普通的隕石,左二是合金隕石,它的體積更大同時需要兩發子彈才能將它擊毀,在被擊中一次後會退變成普通隕石的形態。左三是紅色的隕石,它的速度更快,並且它不是垂直朝下降落的,會有隨機的斜率,考慮到戰機的子彈總是垂直向上發射,斜飛的紅色隕石會更有威脅。

2. 增加遊戲排名系統,玩家在結束一輪遊戲後可以輸入自己的名字,系統會保存玩家的得分,並且會顯示前十名玩家的得分排行榜。這部分功能的實現得益於 Framer 豐富的擴展庫, 西方的 Marckrenn 同志為 Framer 做了一個讀寫 Firebase 的 module :Framer-Firebase ,讓原型工具進行在線數據存儲和讀寫成為可能。

https://img.zl-hj.com/images/2017/6/4/upload/577344a40cf26eb8c9cb7d1d_a4dad7e7-05cd-4c45-b21e-8d36c214193e.jpg/s150070

3. 加上 BGM 和子彈音效, Framer 官方文檔中沒有單獨的聲音圖層說明,但是直接使用 new Audio 就可以新建一個聲音圖層,具體聲音圖層屬性的設置可以參考這裡 Framer-Audio

根據以上三個方向我對遊戲的基礎版進行了改進,試玩和源碼下載:飛機大戰-進階版 (注意會有聲音播放)


對於某個細節的動效比較適合用Framer來做,它的優點是能放在手機上直接操作體驗。如果做多頁面跳轉邏輯,還是axure比較適合。

我寫過幾篇相關的文章,具體你可以參考下面這個回答中的鏈接http://www.zhihu.com/question/24325481/answer/107146875。


推薦閱讀:

Daily Design 設計師武功之「極速入門Framer」
用 Framer Studio (framerjs) 做交互原型的體驗如何?

TAG:交互設計 | 原型工具 | 交互工具 | Framer |