Slide 1

Slide 1 text

Lua文化の伝承!? WFSにおけるイベントスクリプト活用術 〜すべてはより良いコンテンツ制作のために〜 2020/9/18 ⻄⽥ 綾佑

Slide 2

Slide 2 text

西田 綾佑 (Nishida Ryosuke) WFS / Technical Direction @hosi_mo 2014年 東京大学大学院 情報理工学系研究科修了 株式会社 WFS

Slide 3

Slide 3 text

消滅都市 (2014/5/26~) アナザーエデン (2017/4/12~) ほか複数 2014年2月21日設立 現在7年目のゲームスタジオ 株式会社 WFS

Slide 4

Slide 4 text

Lua

Slide 5

Slide 5 text

プレイ動画

Slide 6

Slide 6 text

目次 • 既存資産のイベントスクリプトの紹介 • 新たなスクリプトシステムの構築

Slide 7

Slide 7 text

目次 • 既存資産のイベントスクリプトの紹介 • 新たなスクリプトシステムの構築

Slide 8

Slide 8 text

フィールド 部屋の中へ移動 バルーンを表示 タップするとプレイヤーのHPを満タンにする

Slide 9

Slide 9 text

シナリオ ストーリーNまで到達していたら、 クエスト解放

Slide 10

Slide 10 text

バトル 特定のボスでHPが一定以上削れたら 特殊ギミック発動 等

Slide 11

Slide 11 text

デモ 実装を見てみましょう

Slide 12

Slide 12 text

二度寝

Slide 13

Slide 13 text

鬼ごっこ

Slide 14

Slide 14 text

余談 鬼のようなミニゲームスクリプト

Slide 15

Slide 15 text

製作体制 フィールド シナリオ バトル - シナリオ演出 - キャラ劇の演出 - フィールド作成 (マップエディタ) - フィールドギミック組み込み - フィールドパラメータ調整 - fogや天候調整 - シェーダプログラム調整 - レベルデザイン - バトルギミック設計 レベルデザインから 実装まで行う 演出から 実装まで行う フィールドデザインから 実装まで行う

Slide 16

Slide 16 text

製作体制 チーム内に イベントスクリプト(Lua)を記述できる人が多数 人によってそれぞれに味わい深いイベントが組まれる

Slide 17

Slide 17 text

イベントスクリプト ランタイム(ゲーム) APIリスト ランタイムがLuaに公開してるAPI数 500以上 バトル カメラ制御 フィールドオブジェクト制御 UI アイテム サウンド、ムービー ポストエフェクト 移動 フラグ - 約 50 API - 約 30 API - 約 150 API - 約 20 API - 約 10 API - 約 20 API - 約 100 API - 約 30 API - 約 100 API

Slide 18

Slide 18 text

スクリプトの規模 リリース時(2017年4月) : 63万行 20170531 709077 +72238 20170630 804724 +95647 20170731 861798 +57074 1年で100万行以上のペース 現在(2020年9月) : 443万行

Slide 19

Slide 19 text

イベントスクリプトに 最大限の裁量を

Slide 20

Slide 20 text

目次 • 既存資産のイベントスクリプトの紹介 • 新たなスクリプトシステムの構築

Slide 21

Slide 21 text

のまえに

Slide 22

Slide 22 text

反省点

Slide 23

Slide 23 text

反省点 • 習熟に時間がかかる • プログラマ向けのインターフェース • キャラクターと会話するために準備することが多い • 血塗られたバルオキー

Slide 24

Slide 24 text

24 キャラクターと会話するためのコード

Slide 25

Slide 25 text

25 — イベントシーンアクティベート用フラグ local talk_start = false — エリアに入ると呼ばれる local function regist(time) — フィールドの特定オブジェクトにタッチすると Object_setTouchCB( “オブジェクト名”, function () — フラグが上がる talk_start = true end ) end -- trueを返すとイベントシーン起動 local function activate(time, state) if state ~= "update" then return false end return talk_start end キャラクターと会話するためのコード

Slide 26

Slide 26 text

26 — イベントシーンアクティベート用フラグ local talk_start = false — エリアに入ると呼ばれる local function regist(time) — フィールドの特定オブジェクトにタッチすると Object_setTouchCB( “オブジェクト名”, function () — フラグが上がる talk_start = true end ) end -- trueを返すとイベントシーン起動 local function activate(time, state) if state ~= "update" then return false end return talk_start end キャラクターと会話するためのコード

Slide 27

Slide 27 text

27 — イベントシーンアクティベート用フラグ local talk_start = false — エリアに入ると呼ばれる local function regist(time) — フィールドの特定オブジェクトにタッチすると Object_setTouchCB( “オブジェクト名”, function () — フラグが上がる talk_start = true end ) end -- trueを返すとイベントシーン起動 local function activate(time, state) if state ~= "update" then return false end return talk_start end キャラクターと会話するためのコード

Slide 28

Slide 28 text

28 — イベントシーンアクティベート用フラグ local talk_start = false — エリアに入ると呼ばれる local function regist(time) — フィールドの特定オブジェクトにタッチすると Object_setTouchCB( “オブジェクト名”, function () — フラグが上がる talk_start = true end ) end -- trueを返すとイベントシーン起動 local function activate(time, state) if state ~= "update" then return false end return talk_start end キャラクターと会話するためのコード

Slide 29

Slide 29 text

29 — イベントシーンアクティベート用フラグ local talk_start = false — エリアに入ると呼ばれる local function regist(time) — フィールドの特定オブジェクトにタッチすると Object_setTouchCB( “オブジェクト名”, function () — フラグが上がる talk_start = true end ) end -- trueを返すとイベントシーン起動 local function activate(time, state) if state ~= "update" then return false end return talk_start end キャラクターと会話するためのコード

Slide 30

Slide 30 text

30 キャラクターと会話するためのコード — イベントシーン開始されたらseqテーブルを実行 local function update(time) if seq:exec(time) == Enum_SequenceState.FINISH then return true end return false end — seqテーブル定義 local seq = Sequence_create("main", funcTable) — seqテーブル中身 local funcTable = { CustomSeq_faceTo(label_statue_man, CBO_PARTY1), CustomSeq_faceTo(CBO_PARTY1, label_statue_man), Sequence_setDirB(CBO_PARTY1), Sequence_invoke( function() Object_talkWithOptions( label_statue_man, [[なあ見ろよ アルド。この石像の 素晴らしさ! まさに人類の英知と 生命の崇高さに感動するね!]], {talkerDisplayName = "若者"} )

Slide 31

Slide 31 text

31 キャラクターと会話するためのコード — イベントシーン開始されたらseqテーブルを実行 local function update(time) if seq:exec(time) == Enum_SequenceState.FINISH then return true end return false end — seqテーブル定義 local seq = Sequence_create("main", funcTable) — seqテーブル中身 local funcTable = { CustomSeq_faceTo(label_statue_man, CBO_PARTY1), CustomSeq_faceTo(CBO_PARTY1, label_statue_man), Sequence_setDirB(CBO_PARTY1), Sequence_invoke( function() Object_talkWithOptions( label_statue_man, [[なあ見ろよ アルド。この石像の 素晴らしさ! まさに人類の英知と 生命の崇高さに感動するね!]], {talkerDisplayName = "若者"} )

Slide 32

Slide 32 text

わかりましたね

Slide 33

Slide 33 text

わかりませんね 😷

Slide 34

Slide 34 text

34 血塗られたバルオキー --[[ かくれんボーイ イベント フィーネさらわれている時以外&子供のかくれんぼイベントフラグが1-2の時 、かくれんぼ坊やをアクティベート ]]— if Common_getGlobalFlag(global_flag_baruoki_kakurenboy) == 1 or Common_getGlobalFlag(global_flag_baruoki_kakurenboy) == 2 then if Common_isStoryStepComplete(story_step_story_step_ch1_12) == false or Common_isStoryStepComplete(story_step_story_step_ch11_1) then Common_registAreaEvent("kakurenboy", false) end end

Slide 35

Slide 35 text

35 血塗られたバルオキー --[[ かくれんボーイ イベント フィーネさらわれている時以外&子供のかくれんぼイベントフラグが1-2の時 、かくれんぼ坊やをアクティベート ]]— if Common_getGlobalFlag(global_flag_baruoki_kakurenboy) == 1 or Common_getGlobalFlag(global_flag_baruoki_kakurenboy) == 2 then if Common_isStoryStepComplete(story_step_story_step_ch1_12) == false or Common_isStoryStepComplete(story_step_story_step_ch11_1) then Common_registAreaEvent("kakurenboy", false) end end

Slide 36

Slide 36 text

36 血塗られたバルオキー --[[ かくれんボーイ イベント フィーネさらわれている時以外&子供のかくれんぼイベントフラグが1-2の時 、かくれんぼ坊やをアクティベート ]]— if Common_getGlobalFlag(global_flag_baruoki_kakurenboy) == 1 or Common_getGlobalFlag(global_flag_baruoki_kakurenboy) == 2 then if Common_isStoryStepComplete(story_step_story_step_ch1_12) == false or Common_isStoryStepComplete(story_step_story_step_ch11_1) then Common_registAreaEvent("kakurenboy", false) end end

Slide 37

Slide 37 text

37 血塗られたバルオキー

Slide 38

Slide 38 text

目次 • 既存資産のイベントスクリプトの紹介 • 新たなスクリプトシステムの構築

Slide 39

Slide 39 text

目次 • 既存資産のイベントスクリプトの紹介 • 新たなスクリプトシステムの構築

Slide 40

Slide 40 text

論点 • アナザーエデンで得た教訓を生かす • より馴染みやすく • より書きやすく • こわくないよ

Slide 41

Slide 41 text

言い換えると • 習熟度が低い人でも貢献できる

Slide 42

Slide 42 text

はじめてスクリプトを書く人でも大丈夫

Slide 43

Slide 43 text

はじめてスクリプトを書く人でも大丈夫 • 上から順番に実行されるだけの環境 • まずはアドベンチャーパートの制作を例に

Slide 44

Slide 44 text

44 上から順番に進む記述方法 Log(“はじまり”) Wait(0.5) Log(“0.5秒たちました”) WaitTap() Log(“タップしましたね”) Exit()

Slide 45

Slide 45 text

45 上から順番に進む記述方法 Log(“はじまり”) Wait(0.5) Log(“0.5秒たちました”) WaitTap() Log(“タップしましたね”) Exit()

Slide 46

Slide 46 text

46 上から順番に進む記述方法 Log(“はじまり”) Wait(0.5) Log(“0.5秒たちました”) WaitTap() Log(“タップしましたね”) Exit()

Slide 47

Slide 47 text

47 上から順番に進む記述方法 Log(“はじまり”) Wait(0.5) Log(“0.5秒たちました”) WaitTap() Log(“タップしましたね”) Exit()

Slide 48

Slide 48 text

48 上から順番に進む記述方法 Log(“はじまり”) Wait(0.5) Log(“0.5秒たちました”) WaitTap() Log(“タップしましたね”) Exit()

Slide 49

Slide 49 text

49 上から順番に進む記述方法 Log(“はじまり”) Wait(0.5) Log(“0.5秒たちました”) WaitTap() Log(“タップしましたね”) Exit()

Slide 50

Slide 50 text

50 上から順番に進む記述方法 Log(“はじまり”) Wait(0.5) Log(“0.5秒たちました”) WaitTap() Log(“タップしましたね”) Exit()

Slide 51

Slide 51 text

51 朝のシーン local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit()

Slide 52

Slide 52 text

52 朝のシーン local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit()

Slide 53

Slide 53 text

53 朝のシーン local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit()

Slide 54

Slide 54 text

54 朝のシーン local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit()

Slide 55

Slide 55 text

55 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit() 朝のシーン

Slide 56

Slide 56 text

56 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit() 朝のシーン

Slide 57

Slide 57 text

57 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit() 朝のシーン

Slide 58

Slide 58 text

58 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit() 朝のシーン

Slide 59

Slide 59 text

59 イベント発火を簡単にする function init() TouchEvent("ベッド", function() -- タッチ後にAdv開始 Yield(“lua/Adv/001.lua“) end) end

Slide 60

Slide 60 text

60 イベント発火を簡単にする function init() TouchEvent(“ベッド", function() -- タッチ後にAdv開始 Yield(“lua/Adv/001.lua“) end) end

Slide 61

Slide 61 text

61 イベント発火を簡単にする function init() TouchEvent(“ベッド", function() -- タッチ後にAdv開始 Yield(“lua/Adv/001.lua“) end) end

Slide 62

Slide 62 text

62 イベント発火を簡単にする function init() TouchEvent(“ベッド", function() -- タッチ後にAdv開始 Yield(“lua/Adv/001.lua“) end) end

Slide 63

Slide 63 text

63 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit() 朝のシーン

Slide 64

Slide 64 text

64 イベント発火を簡単にする function init() TouchEvent("オブジェクト", Sample2) end function Sample2() -- タッチ後の処理 Adv.Talk(“にしだ”, [[はーい]]) Wait(0.5) Adv.Talk(“にしだ”, [[0.5秒待ったよ]]) end

Slide 65

Slide 65 text

65 イベント発火を簡単にする function init() TouchEvent("オブジェクト", Sample2) end function Sample2() -- タッチ後の処理 Adv.Talk(“にしだ”, [[はーい]]) Wait(0.5) Adv.Talk(“にしだ”, [[0.5秒待ったよ]]) end

Slide 66

Slide 66 text

66 イベント発火を簡単にする function init() TouchEvent("オブジェクト", Sample2) end function Sample2() -- タッチ後の処理 Adv.Talk(“にしだ”, [[はーい]]) Wait(0.5) Adv.Talk(“にしだ”, [[0.5秒待ったよ]]) end

Slide 67

Slide 67 text

こわくないよ • IDEによるコード保管 • ライブテンプレート

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

ホットリロード • ゲーム起動中にShift + RですぐにLuaをリロード • Unity Editor再生したままでもok • インポート不要

Slide 70

Slide 70 text

ジャンプポイント • ゲーム起動中にescを押すことでDebugPoint()までジャンプ • 指定の箇所まで読み飛ばして • 修正した箇所から再生したい ジャンプ前のコードはすべて正常に動いていることを保証

Slide 71

Slide 71 text

71 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit() 朝のシーン

Slide 72

Slide 72 text

72 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい|いいえ]]) if select == 0 then Adv.talk(aldo, [[おはよう]]) elseif select == 1 then Adv.talk(aldo, [[zzz]]) end Exit() 朝のシーン

Slide 73

Slide 73 text

73 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) local select = Adv.question([[起きる?]], [[はい]]) Adv.talk(aldo, [[おはよう]]) Exit() 朝のシーン

Slide 74

Slide 74 text

74 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) DebugPoint() local select = Adv.question([[起きる?]], [[はい]]) Adv.talk(aldo, [[おはよう]]) Exit() 朝のシーン

Slide 75

Slide 75 text

75 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) DebugPoint() local select = Adv.question([[起きる?]], [[はい]]) Adv.talk(aldo, [[おはよう]]) Exit() 朝のシーン

Slide 76

Slide 76 text

76 local aldo = Adv.character("Aldo", "Default") local fine = Adv.character("Fine", "Default") -- ADV開始 Adv.talk(“???”, [[起きて]]) Adv.talk(aldo, [[…ん?]]) Adv.talk(fine, [[起きてってば]]) -- 0.5秒まつ Wait(0.5) DebugPoint() local select = Adv.question([[起きる?]], [[はい]]) Adv.talk(aldo, [[おはよう]]) Exit() 朝のシーン

Slide 77

Slide 77 text

こわくないね • ホットリロード • ジャンプポイント

Slide 78

Slide 78 text

Luaのカバー範囲 • アドベンチャーパート • フィールド演出 • バトル • メインストーリー

Slide 79

Slide 79 text

ゲームエンジンとコンテンツ ゲームエンジン Advパート ギミック 消滅都市 Cocos2d-x Excel なし アナザーエデン Cocos2d-x Lua Lua ほか運営中タイトル Cocos2d-x Excel Luaほか • ほかタイトル群は権利の都合上表示しておりません

Slide 80

Slide 80 text

x

Slide 81

Slide 81 text

イベントスクリプト ランタイム(ゲームシステム) • アドベンチャーパート • 立ち絵 • UI • サウンド • フィールド • カメラ制御 • オブジェクト制御 • ポストエフェクト • バトル • タイムライン連携 • 会話演出 • メインストーリー • Scene制御 • ほか <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI -> <- 大量のAPI ->

Slide 82

Slide 82 text

論点 • vs. ビジュアルスクリプティング

Slide 83

Slide 83 text

No content

Slide 84

Slide 84 text

www.wfs.games

Slide 85

Slide 85 text

www.wfs.games

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

Lua

Slide 88

Slide 88 text

引き続き、RPG開発に全力で挑みます!

Slide 89

Slide 89 text

No content