gemma-4-E2B-it-GGUF 在 Unsloth 上表現異常良好的秘密

最近很常用 Ollama 來嘗試各種流行的 Local LLM,
但發現 Gemma4 E2B 跟 E4B 明明參數比上一代 Gemma3 的小參數模型大上不少,
用起來的感覺卻很奇怪,簡單的指令都不太能好好執行。

後來因緣際會下載了 Unsloth 來玩,
發現這個 Model 在 Unsloth 的做簡單的搜尋表現非常亮眼,
反應很快,回答的品質也很高。
當然 Model 本身的參數我也看了一下,
除了 context window 設得特別低,其他也沒什麼特別。

倒是 Chat Template 長得很魔幻(?):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
{%- macro format_parameters(properties, required, filter_keys=false) -%}
{%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
{%- set ns = namespace(found_first=false) -%}
{%- for key, value in properties | dictsort -%}
{%- set add_comma = false -%}
{%- if not filter_keys or key not in standard_keys -%}
{%- if ns.found_first %},{% endif -%}
{%- set ns.found_first = true -%}
{{ key }}:{
{%- if value['description'] -%}
description:<|"|>{{ value['description'] }}<|"|>
{%- set add_comma = true -%}
{%- endif -%}
{%- if value['type'] | upper == 'STRING' -%}
{%- if value['enum'] -%}
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
enum:{{ format_argument(value['enum']) }}
{%- endif -%}
{%- elif value['type'] | upper == 'ARRAY' -%}
{%- if value['items'] is mapping and value['items'] -%}
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
items:{
{%- set ns_items = namespace(found_first=false) -%}
{%- for item_key, item_value in value['items'] | dictsort -%}
{%- if item_value is not none -%}
{%- if ns_items.found_first %},{% endif -%}
{%- set ns_items.found_first = true -%}
{%- if item_key == 'properties' -%}
properties:{
{%- if item_value is mapping -%}
{{- format_parameters(item_value, value['items']['required'] | default([])) -}}
{%- endif -%}
}
{%- elif item_key == 'required' -%}
required:[
{%- for req_item in item_value -%}
<|"|>{{- req_item -}}<|"|>
{%- if not loop.last %},{% endif -%}
{%- endfor -%}
]
{%- elif item_key == 'type' -%}
{%- if item_value is string -%}
type:{{ format_argument(item_value | upper) }}
{%- else -%}
type:{{ format_argument(item_value | map('upper') | list) }}
{%- endif -%}
{%- else -%}
{{ item_key }}:{{ format_argument(item_value) }}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
}
{%- endif -%}
{%- endif -%}
{%- if value['nullable'] %}
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
nullable:true
{%- endif -%}
{%- if value['type'] | upper == 'OBJECT' -%}
{%- if value['properties'] is defined and value['properties'] is mapping -%}
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
properties:{
{{- format_parameters(value['properties'], value['required'] | default([])) -}}
}
{%- elif value is mapping -%}
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
properties:{
{{- format_parameters(value, value['required'] | default([]), filter_keys=true) -}}
}
{%- endif -%}
{%- if value['required'] -%}
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
required:[
{%- for item in value['required'] | default([]) -%}
<|"|>{{- item -}}<|"|>
{%- if not loop.last %},{% endif -%}
{%- endfor -%}
]
{%- endif -%}
{%- endif -%}
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
type:<|"|>{{ value['type'] | upper }}<|"|>}
{%- endif -%}
{%- endfor -%}
{%- endmacro -%}
{%- macro format_function_declaration(tool_data) -%}
declaration:{{- tool_data['function']['name'] -}}{description:<|"|>{{- tool_data['function']['description'] -}}<|"|>
{%- set params = tool_data['function']['parameters'] -%}
{%- if params -%}
,parameters:{
{%- if params['properties'] -%}
properties:{ {{- format_parameters(params['properties'], params['required']) -}} },
{%- endif -%}
{%- if params['required'] -%}
required:[
{%- for item in params['required'] -%}
<|"|>{{- item -}}<|"|>
{{- ',' if not loop.last -}}
{%- endfor -%}
],
{%- endif -%}
{%- if params['type'] -%}
type:<|"|>{{- params['type'] | upper -}}<|"|>}
{%- endif -%}
{%- endif -%}
{%- if 'response' in tool_data['function'] -%}
{%- set response_declaration = tool_data['function']['response'] -%}
,response:{
{%- if response_declaration['description'] -%}
description:<|"|>{{- response_declaration['description'] -}}<|"|>,
{%- endif -%}
{%- if response_declaration['type'] | upper == 'OBJECT' -%}
type:<|"|>{{- response_declaration['type'] | upper -}}<|"|>}
{%- endif -%}
{%- endif -%}
}
{%- endmacro -%}
{%- macro format_argument(argument, escape_keys=True) -%}
{%- if argument is string -%}
{{- '<|"|>' + argument + '<|"|>' -}}
{%- elif argument is boolean -%}
{{- 'true' if argument else 'false' -}}
{%- elif argument is mapping -%}
{{- '{' -}}
{%- set ns = namespace(found_first=false) -%}
{%- for key, value in argument | dictsort -%}
{%- if ns.found_first %},{% endif -%}
{%- set ns.found_first = true -%}
{%- if escape_keys -%}
{{- '<|"|>' + key + '<|"|>' -}}
{%- else -%}
{{- key -}}
{%- endif -%}
:{{- format_argument(value, escape_keys=escape_keys) -}}
{%- endfor -%}
{{- '}' -}}
{%- elif argument is sequence -%}
{{- '[' -}}
{%- for item in argument -%}
{{- format_argument(item, escape_keys=escape_keys) -}}
{%- if not loop.last %},{% endif -%}
{%- endfor -%}
{{- ']' -}}
{%- else -%}
{{- argument -}}
{%- endif -%}
{%- endmacro -%}
{%- macro strip_thinking(text) -%}
{%- set ns = namespace(result='') -%}
{%- for part in text.split('<channel|>') -%}
{%- if '<|channel>' in part -%}
{%- set ns.result = ns.result + part.split('<|channel>')[0] -%}
{%- else -%}
{%- set ns.result = ns.result + part -%}
{%- endif -%}
{%- endfor -%}
{{- ns.result | trim -}}
{%- endmacro -%}

{%- macro format_tool_response_block(tool_name, response) -%}
{{- '<|tool_response>' -}}
{%- if response is mapping -%}
{{- 'response:' + tool_name + '{' -}}
{%- for key, value in response | dictsort -%}
{{- key -}}:{{- format_argument(value, escape_keys=False) -}}
{%- if not loop.last %},{% endif -%}
{%- endfor -%}
{{- '}' -}}
{%- else -%}
{{- 'response:' + tool_name + '{value:' + format_argument(response, escape_keys=False) + '}' -}}
{%- endif -%}
{{- '<tool_response|>' -}}
{%- endmacro -%}

{%- set ns = namespace(prev_message_type=None) -%}
{%- set loop_messages = messages -%}
{{- bos_token -}}
{#- Handle System/Tool Definitions Block -#}
{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
{{- '<|turn>system\n' -}}
{#- Inject Thinking token at the very top of the FIRST system turn -#}
{%- if enable_thinking is defined and enable_thinking -%}
{{- '<|think|>\n' -}}
{%- set ns.prev_message_type = 'think' -%}
{%- endif -%}
{%- if messages[0]['role'] in ['system', 'developer'] -%}
{%- if messages[0]['content'] is string -%}
{{- messages[0]['content'] | trim -}}
{%- elif messages[0]['content'] is sequence -%}
{%- for item in messages[0]['content'] -%}
{{- item['text'] | trim + ' '-}}
{%- endfor -%}
{%- endif -%}
{%- set loop_messages = messages[1:] -%}
{%- endif -%}
{%- if tools -%}
{%- for tool in tools %}
{{- '<|tool>' -}}
{{- format_function_declaration(tool) | trim -}}
{{- '<tool|>' -}}
{%- endfor %}
{%- set ns.prev_message_type = 'tool' -%}
{%- endif -%}
{{- '<turn|>\n' -}}
{%- endif %}

{#- Pre-scan: find last user message index for reasoning guard -#}
{%- set ns_turn = namespace(last_user_idx=-1) -%}
{%- for i in range(loop_messages | length) -%}
{%- if loop_messages[i]['role'] == 'user' -%}
{%- set ns_turn.last_user_idx = i -%}
{%- endif -%}
{%- endfor -%}

{#- Loop through messages -#}
{%- for message in loop_messages -%}
{%- if message['role'] != 'tool' -%}
{%- set ns.prev_message_type = None -%}
{%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
{#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}
{%- set prev_nt = namespace(role=None, found=false) -%}
{%- if loop.index0 > 0 -%}
{%- for j in range(loop.index0 - 1, -1, -1) -%}
{%- if not prev_nt.found -%}
{%- if loop_messages[j]['role'] != 'tool' -%}
{%- set prev_nt.role = loop_messages[j]['role'] -%}
{%- set prev_nt.found = true -%}
{%- endif -%}
{%- endif -%}
{%- endfor -%}
{%- endif -%}
{%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%}
{%- if not continue_same_model_turn -%}
{{- '<|turn>' + role + '\n' }}
{%- endif -%}

{#- Render reasoning/reasoning_content as thinking channel -#}
{%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
{%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}
{{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
{%- endif -%}

{%- if message['tool_calls'] -%}
{%- for tool_call in message['tool_calls'] -%}
{%- set function = tool_call['function'] -%}
{{- '<|tool_call>call:' + function['name'] + '{' -}}
{%- if function['arguments'] is mapping -%}
{%- set ns_args = namespace(found_first=false) -%}
{%- for key, value in function['arguments'] | dictsort -%}
{%- if ns_args.found_first %},{% endif -%}
{%- set ns_args.found_first = true -%}
{{- key -}}:{{- format_argument(value, escape_keys=False) -}}
{%- endfor -%}
{%- elif function['arguments'] is string -%}
{{- function['arguments'] -}}
{%- endif -%}
{{- '}<tool_call|>' -}}
{%- endfor -%}
{%- set ns.prev_message_type = 'tool_call' -%}
{%- endif -%}

{%- set ns_tr_out = namespace(flag=false) -%}
{%- if message.get('tool_responses') -%}
{#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
{%- for tool_response in message['tool_responses'] -%}
{{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}
{%- set ns_tr_out.flag = true -%}
{%- set ns.prev_message_type = 'tool_response' -%}
{%- endfor -%}
{%- elif message.get('tool_calls') -%}
{#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#}
{%- set ns_tool_scan = namespace(stopped=false) -%}
{%- for k in range(loop.index0 + 1, loop_messages | length) -%}
{%- if ns_tool_scan.stopped -%}
{%- elif loop_messages[k]['role'] != 'tool' -%}
{%- set ns_tool_scan.stopped = true -%}
{%- else -%}
{%- set follow = loop_messages[k] -%}
{#- Resolve tool_call_id to function name -#}
{%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}
{%- for tc in message['tool_calls'] -%}
{%- if tc.get('id') == follow.get('tool_call_id') -%}
{%- set ns_tname.name = tc['function']['name'] -%}
{%- endif -%}
{%- endfor -%}
{#- Handle content as string or content-parts array -#}
{%- set tool_body = follow.get('content') -%}
{%- if tool_body is string -%}
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
{%- elif tool_body is sequence and tool_body is not string -%}
{%- set ns_txt = namespace(s='') -%}
{%- for part in tool_body -%}
{%- if part.get('type') == 'text' -%}
{%- set ns_txt.s = ns_txt.s + (part.get('text') | default('')) -%}
{%- endif -%}
{%- endfor -%}
{{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
{%- else -%}
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
{%- endif -%}
{%- set ns_tr_out.flag = true -%}
{%- set ns.prev_message_type = 'tool_response' -%}
{%- endif -%}
{%- endfor -%}
{%- endif -%}

{%- set captured_content -%}
{%- if message['content'] is string -%}
{%- if role == 'model' -%}
{{- strip_thinking(message['content']) -}}
{%- else -%}
{{- message['content'] | trim -}}
{%- endif -%}
{%- elif message['content'] is sequence -%}
{%- for item in message['content'] -%}
{%- if item['type'] == 'text' -%}
{%- if role == 'model' -%}
{{- strip_thinking(item['text']) -}}
{%- else -%}
{{- item['text'] | trim -}}
{%- endif -%}
{%- elif item['type'] == 'image' -%}
{{- '<|image|>' -}}
{%- set ns.prev_message_type = 'image' -%}
{%- elif item['type'] == 'audio' -%}
{{- '<|audio|>' -}}
{%- set ns.prev_message_type = 'audio' -%}
{%- elif item['type'] == 'video' -%}
{{- '<|video|>' -}}
{%- set ns.prev_message_type = 'video' -%}
{%- endif -%}
{%- endfor -%}
{%- endif -%}
{%- endset -%}

{{- captured_content -}}
{%- set has_content = captured_content | trim | length > 0 -%}

{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
{{- '<|tool_response>' -}}
{%- elif not (ns_tr_out.flag and not has_content) -%}
{{- '<turn|>\n' -}}
{%- endif -%}
{%- endif -%}
{%- endfor -%}

{%- if add_generation_prompt -%}
{%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
{{- '<|turn>model\n' -}}
{%- endif -%}
{%- endif -%}

竟然花了這麼長的篇幅在防呆跟實作 Reasoning 啊……
把這個塞進 Ollama 他就能變聰明嗎 (懶得嘗試 沒有下回待續)

舊版 Ruby 在 M1 上 Compile 遇到的 libffi 問題

最近因為某些原因又要把舊專案拿出來用,Ruby 2.5.5 沒想到也已經那麼久了。不知為何好像只有 Apple ARM 上的 macOS 才會有 compile 的問題。查了一下 log 發現以下訊息:


closure.c:264:14: error: implicit declaration of function ‘ffi_prep_closure’ is invalid in C99 [-Werror,-Wimplicit-function-declaration]


result = ffi_prep_closure(pcl, cif, callback, (void *)self);


看起來是找不到 ffi 的相關函式,查了一下 brew info libffi 後得到以下訊息:


libffi is keg-only, which means it was not symlinked into /opt/homebrew,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.


For compilers to find libffi you may need to set:
export LDFLAGS=”-L/opt/homebrew/opt/libffi/lib”
export CPPFLAGS=”-I/opt/homebrew/opt/libffi/include”


For pkg-config to find libffi you may need to set:
export PKG_CONFIG_PATH=”/opt/homebrew/opt/libffi/lib/pkgconfig”


簡單來說就是 homebrew 的 libffi 好像會跟 macOS 的衝突,需要手動設定環境變數讓 rvm make ruby 的時候可以順利使用 libffi。把上述三個 export 下完之後再次 rvm install 2.5.5 就完成啦!


幣圈走一遭 - 2017 與 2021 的加密貨幣生態差異紀事

加密貨幣各在 2017 與 2021 年到達了大家都沒預料到的漲幅,很慶幸的是自己有參與到這兩次盛事,且因此有一些微薄的投資利潤。


自己最一開始買加密貨幣是 2016 年,為了跨國的網路購物,當時只是覺得新奇,然後不想什麼消費都使用信用卡留名字,感覺很不舒服。當時印象中比特幣一顆才 $600 美元吧。


雖說看好比特幣能夠為跨國支付帶來不錯的影響,但怎麼也沒想到 2017 年的夏天,比特幣來到了 $4,000 美元大關——我還是看到科技新聞才知道這件事情,在我 wallet 裡的那些餘額已經有當初支付出去的價值,我第一次覺得自己有投資的眼光(的錯覺)。


BestChange: 2017 年 DeFi 還沒有興起時,都在這邊找交易所去換一些主流貨幣。

2017 年玩加密貨幣是一件相對單純的事情,台灣 2016 就已經有 bitoex 可以在超商購買比特幣了,而且那時記得好像還不需要身分驗證,也就是可以匿名購買,如果你更注重隱私一點,連 Tor 繞個圈建立錢包好像也是可行的方式。那時我去 LocalBitcoin 找了當地有買比特幣的買家,用儲蓄買了一點比特幣,並且在線上找評價不錯的交易所轉換成注重隱私的門羅幣。當年的年末,也靠著這筆投資賺取了一些生活費。而翌年初因為中國禁令導致加密貨幣市場進入了漫長的冰河期,我也就去做別的事情了。


2021 年市場再度熱絡起來,我憑著以前在幣圈裡剩下的餘額進行投資,一邊研究才一邊發現,現在並非只是大家創建了一堆貨幣,看那邊陣營聲勢比較猛,跟 2017 年有了很多不同之處:


這三年多來到底多了些什麼


Binance Smart Chain - 幣安鏈


幣安交易所在一開始推出時,甚至一度有人在討論是不是個詐騙交易所,沒想到之後不但發行了自己的 BNB 幣,社群還發展了以 BNB 為中心的 BSC(Binance Smart Chain),目前市值僅次於 Ethernet 的去中心化網路。以往講到 Dapp 大概就是指運行在以太坊網路上的合約程式,然而隨著用戶跟交易機器人增加,gas fee 很高還偶爾會有當機情形發生一直是小資玩家最詬病的痛點,BSC 不但速度不錯,交易費也相對便宜許多,而 BEP-20 也同樣能使許多新創產業能夠發行自己的代幣,用自己的商業模式吸引人來投資。


而除了 BSC 之外,與 Ethernet 相容的側鏈 Polygon NetworkFantom Opera Network 聲勢也很旺,擴充鏈的 Optimism 以及未來的 Arbitrum,與以太坊無關、強調安全穩定的 Solana,整合這些網路的跨鏈 solution 包括 Polkadot,以及 oracle 系統 Chainlink 等,都是去中心化非常重要的基礎建設,但入門知識門檻也相對較高。這是 2017 年唯有 Ethereum Mainnet 獨大的世界完全沒辦法相比的生態。


DeFi (Decentralized Finance) - 去中心化金融機構


去中心化網路最初發展的目的,無非就是希望擺脫某些機構 / 政府的掌控,讓自由能夠透過網路的普及觸及到世界的各個角落。而這個概念在加密貨幣市場發展的結果,就是人人都可以開一間自己的金融機構,發行自己的貨幣,決定自己的發展目標。


隨著自動化交易中心 UniSwap 在 Ethernet 的成功,許多人都開始複製他們的程式,發行了自己的代幣,並用高額的利率鼓勵大家在自己的 DeFi 中押入加密貨幣資產,造成滿街都是去中心化銀行的現象。其中當然很多是沒有實際價值,只是想找機會詐取用戶錢財的惡質 DeFi 存在,但也有一些企圖解決經濟問題,甚至帶來新概念的 DeFi。


像是 Compound,就解決了傳統借貸可能會找不到人清償的問題,PoolTogether 則是進一步利用 Compound 借貸方存款的利息,達到不賠錢彩券的應用(或者更像不會被倒會的標會)。由於押入的資產被區塊鏈網路給確保了,因此許多以往做不到的保證,或者需要大量時間人力來驗證的事情,都可以被忽略,進而衍生出許多較為可靠的獲利模式


當然這些創新的獲利模式有些或許還存在著商業漏洞,但無論是傳統金融還是加密貨幣,都同樣有著創新與風險的問題,像是傳統金融也有過卡神事件,這並不會因為你否定哪一邊,另外一邊就變得比較安全,只有等待時間過去,才能夠慢慢驗證各種現在看起來很新的獲利模式。


Liquidity Mining - 流動性挖礦


許多 DeFi 為了讓資金不但能留在自己的資金池外還能動態調整價格,因此鼓勵大家投入兩種以上不同的貨幣當作流動資產,相對應的貨幣價格會因為套利者在因應不同市場而做出的套利行為,會自動地做出調整,這個計算方式還有個專有名詞叫做「恆定乘積作市模型 (Constant Product Market Maker Model)」。當然由於這個模型也才在 2018 年問世,是一個相當新的概念,即使裡面的運算方式已經解決了許多現有問題,但也衍生了更多延伸的問題,其中最大的一個問題就是:將資金投入的人會遭受「無常損失 (Impermanent Loss)」。因為投入的資金是浮動的,因此投資者與其將資金投入一個跟自己利益無關的 DeFi 中,還不如自己拿著。而為了解決這個問題,流動性挖礦 (Liquidity Mining) 概念因應而生。投資者將資金對放入 DeFi 資金池中讓大家在裡面交易後,自身可以定期得到一些利息。


這對利率早已如死灰的傳統金融機構而言,是一個很大的挑戰。年利率 0.6% 的定存,跟日利率 1% 且手續費低廉的儲蓄,你要將資金放在哪裡呢?SushiSwap 在 2020 年 Fork 了 Uniswap 的程式碼,並加入流動性挖礦概念後,就捲走了 Uniswap 70% 的流動資金,這件事情也震撼了加密貨幣圈好一段時間。


Flash Loan - 閃電貸


貸款通常是需要抵押資金的,而在區塊鏈世界裡,卻有一種不需要抵押金就可以進行貸款的方式:閃電貸款 (Flash Loan)。原理上是利用區塊鏈在交易失敗時可以進行回溯的特性,將「放貸」與「清算」這兩件事情限制在一個區塊內。只要操作者利用合約程式想辦法在一個區塊時間內完成借款與還款,那麼期間做的所有事情都可以被區塊鏈認證;反之要是無法達成還款,那麼期間做的其他交易都會跟著失敗。


這對於合約程式設計者來說是一個極大的好消息,因為這樣他們可以寫程式在兩個不同的 DeFi 間進行巨額的套利行為。Ethereum Mainnet 曾經有一度在 Uniswap 與 SushiSwap 間,套利者利用 Aave 的閃電貸功能無本獲利了數十萬美金。而後來這項工具更被拿來利用成為一個很難被防禦的閃電貸攻擊 (Flash Loan Attack),在 Binance Smart Chain 曾被攻擊者在一個月內針對 7 個與價格預言機 (Oracles) 同步上有弱點的 DeFi 網站,盜走了總計數億美金的資產,我自己也為了投入某些專案的 IDO,而曾經差點成為這些攻擊與詐騙的受害者。


懸而未解的問題


建立在創新上的創新


剛剛提到的流動性挖礦,是建立在恆定乘積作市數學模型上,產生無常損失的一個解決方案。


在建立在嶄新概念上的全新 solution,聽起來更加的令人不安對吧?這也是目前加密貨幣市場遇到的極大問題,許多新創公司都建立在一個不穩的地基上。實務上流動性挖礦也真的遇到了極大的挑戰。仔細看恆定乘積作市模型,會發現無常損失不只發生在投入的貨幣上漲,連帶下跌時也會發生——貨幣上漲的時候投資者賺得比較少,下跌時卻賠得比較多,即使流動性挖礦給了豐富的利率,也不一定能夠補償這份損失。也就是說,所謂的無常損失並非無常,而是常常。既然常常會有損失,那麼與 DeFi 利益無關的我,有什麼理由要提供資金給他們呢?


無常損失的曲線圖,來源:https://tokentuesdays.substack.com/p/eliminating-impermanent-loss

也有新創 DeFi 會利用資金與團隊不透明的情況,用極高的利率吸引用戶來投入資金,再操作代幣價格來進行捲款(Rug Pull)的行為。甚至有的會在事後發公關稿說他們自己也是受害者,請投資者再投入更多資金來彌補傷害,進而不斷重複這個循環。


閃電貸攻擊也是基於創新而生的弱點,雖然技術上他們的操作完全合乎系統規範,但道德上顯然受到大家的撻伐。所幸目前針對這樣類型的攻擊,已經有資訊安全公司對過於高額的交易進行區塊鏈監測的系統,而竊盜者也通常會怕暴露行蹤而一時之間無法對盜來的資產做進一步處置。


機器人交易


與股市不同的地方是,加密貨幣市場是 24/7 營運的市場,並且隨著各個交易所開始釋出 API,使得撰寫自動交易機器人變成一個門檻很低的事情。當然如此一來,那些養套殺的心機也有可能早已被寫入程式裡。


與 2017 不同的是,今年有許多與利多利空無關消息的漲跌起伏。我常半夜看著盤面心想:此時此刻,引導市場情緒的究竟是人類還是機器人?


有許多現象可以加強這個論點:包括了「浮上檯面(變得有名)的貨幣幣價會以毫秒之差隨著比特幣起伏」、「比特幣的漲跌在某些整數立刻被強力反彈(即使檯面上沒有限價訂單)」,以及「一些 DeFi 的自有貨幣價格會隨著 TVL 的劇烈變動而即時有不正常地起伏」等等。


價值化與去中心網路的過度分散


另外一點可能是加密貨幣的原罪:與美元的對價關係。只要加密貨幣存在著與法定貨幣對價的關係,那麼就勢必會有機構與政府試圖介入,而不穩定的幣價也使得投資人很難能夠將資金長久留在某個網路。


加密貨幣的本質是去中心化網路的一項應用,而對於去中心網路來說,人人都想要建造自己的網路或許是一個很積極正面的消息,畢竟去中心化網路發展了這麼久,也真的是牽涉到資金之後,整個發展才變得快速了起來。只是現在每個網路在做基礎建設時,很難會有穩定的資金,建設出的東西可能就不堪使用,造成未來可能會有很多較小型的網路被 51% attack 給掌控。


但這也帶來了去中心化的根本問題:沒有中央管控的網路,那麼誰來制裁犯罪,誰來定義犯罪呢?DeFi 詐騙與竊盜的案例每個月就會發生好幾起,自由的代價也許就是永恆的警戒吧。


結語:還是有一些好消息


即使加密貨幣市場目前由於鯨魚們的介入,發展尚不明朗 —— 中國不是第一次發布禁令(也不知道他們何時才會發布真的禁令),美國華爾街大老們、說加密貨幣是垃圾的富翁們開始透漏自己擁有「一些」加密貨幣資產(我不知道一億對他們來說算不算一些)—— 對於我們這種小蝦米散戶而言,也不是完全沒有好處:


  • 我們可以很方便地拿一部分儲蓄來投入 / 支持跨國的新創產業,如果運氣好的話,HODL 個幾年,也許可以獲得可觀的回報。
  • 我們可以應用去中心化網路部屬我們的程式 / 放置可以公開的資料,而不需要擔心有天主機商不再運行你的伺服器。
  • 我們可以透過某些加密貨幣 (ZCash、Monero) 達到匿名付款的效果。

我自己有放了一些美金在 Ethereum 的 PoolTogether 裡面,目前版本的 PoolTogether 除了可以有機會中獎之外,還有額外的儲蓄生息功能。雖然也是有一種風險是哪天這個合約被找到目前未知的漏洞的話,也許錢就沒了,但我覺得像是這種可以顛覆傳統金融的應用,是可以去試試看的。


NFT 前陣子話題蠻火熱的,我個人認為 NFT 的本質在於所有權的認證


有資金的地方就會有「無法理解的市場」存在,你覺得我的歌很難聽,我覺得你的詩很無趣,但或許在不同的圈子裡面,這兩樣作品的價值就各自有翻轉的機會。NFT 是網際網路發展之下,為了加速大家在全球找到各自價值市場的一個附加產物,就跟比特幣一樣,本身並不帶有太多價值,比較是價值的載體。版權與使用權的爭議並不會因為 NFT 發展起來就隨著消弭,然而 Copyleft 的長年戰爭會不會因為 NFT 的興起而有所變化,這感覺又是另外一個十年戰場了(扶額)。


獨立遊牧工作生活經驗

一台筆電,甚至一台 Pad 有機會能夠執行許多工作,是生在這個時代一個很大的優勢。如果你是這樣的工作者,那麼也許在生活上會遇到一些相似的問題,這篇跟各位分享這三四年來喜歡四處跑的我在台北的獨立工作的客家型(?)遊牧生活經驗。


工作地點


首先在哪裡工作是一件相當重要的事情。很多人都幻想能在家裡工作,但實務上能夠這樣做的人非常之少,如果你自制力不是這麼的高,或是還有跟家人同住,容易被干擾的話,建議最好還是出門工作會比較好一些,出門的儀式感也能夠讓人有一種開啟一天的感覺。


如果你很在乎工作地點的安靜程度,或者你真的窮到爆炸,那麼各地的圖書館閱覽室絕對是你最好的選擇,現在很多閱覽室都有筆電使用區,有插座可以使用,雖然不是每個分館插座都很多,但是環境不錯,大多都有飲水機這一點是很大的優勢。



圖書館閱覽室
花費:★★★★★ 免費 $0
便利:★★★★☆ 交通不一定方便,通常會有飲水機
舒適:★★★☆☆ 座椅不一定舒適,但非常安靜
插座:★★★☆☆ 除了某些分館每個位子都有插座外,通常插座偏少


如果你跟我一樣不太喜歡在太過安靜的地方工作,那麼成本花費相對低廉的地方就是便利商店了,目前要查到哪些分店有插座不是這麼的方便,可能要花時間在常待的區域自己去店裡看看。



便利商店
花費:★★★★☆ 一杯豆漿 $20,飲水若不自備則需要額外花費
便利:★★★★☆ 交通方便,可以選離家近的地方,現在很多超商都有座位區
舒適:★★☆☆☆ 環境通常比較差,可能會需要自己清潔桌面
插座:★☆☆☆☆ 除了某些新裝潢的店會有比較多插座,通常都要自備電源,或是筆電本身續航力要夠


橘色招牌的家樂福超市幾年前開始大量設點,很多這樣子類型的家樂福超市休息區都有插座可以使用,雖然厚臉皮一點可以直接帶著筆電坐在那,不過建議還是買杯飲料吧。


家樂福 Market
花費:★★★★★ 一杯飲料 $20~$30,通常旁邊有飲水機
便利:★★★★☆ 有機會可以選擇離家近的地方
舒適:★★★☆☆ 座椅還行,整潔度因各店而異
插座:★★★★☆ 插座數量不少


再來就是最常見的咖啡廳了,咖啡廳工作一直是遊牧民族的最愛,不過缺點在於中途想要離開去吃東西之類的再回來,就要再花一杯咖啡的錢,通常有插座可以久坐的店,咖啡也不會太便宜,優點是各家咖啡廳可能會有不同特色跟收藏,有機會激發出一些不同想法。


咖啡廳
花費:★★★☆☆ 一杯咖啡 $100 上下,若要用餐則會直逼 $300
便利:★★★★☆ 有機會可以選擇離家近,或離捷運近的地方,有時飲水需要跟櫃台要
舒適:★★★★☆ 通常還算舒適整潔,但有時遇到很吵的客人會很崩潰
插座:★★★☆☆ 各店插座數量不一定,也是需要在自己常待的地方做調查


如果你的經濟條件不錯,或是最近想奢侈一下,可以考慮一些高級網咖,有機會可以只帶行動硬碟,不用帶筆電,而且還會有肥宅快樂水可以無限暢飲。不過為了身體健康還是喝點茶就好了。建議攜帶酒精清潔鍵盤滑鼠。


高消費網咖
花費:★★☆☆☆ 計時消費相當昂貴,包時可能有機會稍微便宜一點
便利:★★★★★ 大多數網咖都是 24H 營業,就算半夜睡不著想工作都可以,通常會提供餐點飲料,不用再移動覓食
舒適:★★★★★ 高消費的網咖通常環境都維持很好,甚至是寧靜場所,想小憩一下也有個不錯的地方可以躺或淋浴
插座:★★★★★ 每個位子都有插座,爽啦


再來就是可能你工作了一陣子有些存款,甚至已經開始考慮設置行號接些大案子,那麼商業辦公室就是一個很理想的選擇。當然也可以選擇一些近十年來很流行的「共同工作空間」。他的本質上跟商辦很像,但通常會提供一個共享辦公區域,優點是月租通常可以作商業登記,也有行政人員可以幫你代收文件或代辦一些簡易的行政事項,缺點是一旦你租了一個固定的地點,可能就開始要放棄遊牧生活了。


共同工作空間
花費:★☆☆☆☆ 計時消費相當昂貴,包月的話對工作狂來說通常還算划算
便利:★★★★★ 很多地方都有提供飲水、茶飲,甚至有廚房以及淋浴間可使用
舒適:★★★★★ 通常是不錯的辦公環境,有些地方甚至有沙發可以讓你小躺一下
插座:★★★★☆ 插座通常都很多


可進行的工作型態


身為一個斜槓中年,多方嘗試是維持獨立工作熱情的一個有效手段。雖然可能多頭蠟燭燒完多頭空,但沒關係我魯我驕傲!(是這樣說的嗎)


最低成本的就是文字工作了,你的筆電或是平板不需要多好的效能,只要可以打字就有機會能夠進行。數位行銷方面也是只要開得動 social media 就有機會可以完成。實際上即使你不是一個專職的文字工作者,都可以藉由撰寫自己專業領域的 blog 來訓練文字能力,以及敏銳度。


再來就是網頁製作,或者應該說是前端工程師,軟體方面的需求比較沒有這麼高,現在應該沒有筆電裡面沒有裝瀏覽器的對吧?Adobe XD 也是免費的前端設計工具,即使你不是完全專業的 UI/UX 設計師都有機會可以用這套工具來製作一個網頁來宣傳自己。


平面設計的話可能就需要用到一些相對比較昂貴的軟體,其實如果你是專職工作者,我個人還蠻推薦訂閱 Adobe Creative Cloud 的,不太使用盜版軟體的原因一方面雖然是自己懶得找 crack,一方面是我不想成為一個一邊靠北薪水低,一邊不花錢在竊用軟體的人。 XD


音樂製作雖然需要的軟硬體費用相當驚人,但近十年來即使是只有一台筆電的人也能夠製作出相當水準的音樂囉。底下這首曲子就是我用 Propellerhead Reason 9,只用筆電 mic 跟內建音色還有合成器做的曲子,連 keyboard 都沒用,只用滑鼠點音符的作品。



影片剪輯的話嘛…… 筆電跟 SSD 外接硬碟零零總總加起來就要至少十幾萬了,更不用說如果你還得自己攝影的話,器材鐵定是貴到靠北的。這方面如果想客家一波可能目前是有點困難的。不過若是你經濟允許的情況下,也是有機會可以做到帶著這些器材遊牧工作的。


對於一些需要社交的獨立工作者而言,共同工作空間可能會是一個不錯的選擇,當然不是要你去搭訕或打擾那些正在辛苦工作的人,而是這些地方通常會有提供媒合的服務,你可以在某個告示板上寫下你的專長跟聯絡方式,也可以在板上尋找你所需要的合作技能跟對象。


生產工具


筆電的話唯一推薦 MacBook 系列,在使用 Mac 系列電腦之前,我一直以一個 custom PC 的使用者自豪,直到後來發現我花在一些冤枉問題的時間加起來已經夠我成家立業(??),這才乖乖不再對自己小氣。該花的錢,還是花一下吧。


再來就是可以當網卡的手機,以及一個 4G 吃到飽的門號(在為這篇文作結時 5G 時代似乎已經來了)。據我所知很多客家型獨立資訊工作者很不捨得在網路上花錢,這對我來說真的是不可思議,現在有太多事情都需要上網查,即使是資深的工程師如我也沒有辦法只靠 man page 來寫程式或是用 unix tools,不太明白這些人哪來的自信可以不用查資料就做好事情。就算是文字工作者也是偶爾要查個字典吧,如果要去計算流量之類的,所花費的時間恐怕也是夠你成家立業又離婚了,這方面別省好嗎。


遇到需要跟別人協作文件的話就用 Google Docs 吧,不過近年來 Google 的走向有點奇怪,也許可以考慮多使用 Workflowy 來作筆記。


各領域現在有趣的軟體工具實在太多了,樂於使用新工具是好事,但不可以佔用太多時間,安排有限度的時間給使用新工具會是一個有效的方式。這方面有機會再寫一篇跟大家分享好了。


交通手段


除非你家跟你選擇的地點離捷運、公車站近,不然最好還是買台機車吧。有了機車之後其實能夠去的地方就多了不少,選擇一多雖然有時不一定是一種幸福,但遇到某些狀況時,沒得選擇鐵定不是一件令人開心的事。


當然如果你窮到靠北也不是沒有 solution 的,前半年我才發現 UBike 長途通勤也不一定不好,大台北地區的河濱腳踏車系統很發達,空氣還算不錯,而且河濱段不會有號誌,速度比想像中快上很多。工作前可以搭捷運,工作後可以騎車運動,到家順便洗澡,即使是夏天,晚上都還不用煩惱防曬問題。但如果住處到工作地點是會經過上下坡的路段,可能這個方案就比較不適合你。


再來就是共享機車 iRent / WeMo / GoShare,我自己使用了兩年多,覺得還算划算,畢竟如果要自己養車,加上油錢開銷並不低。缺點的話也不是沒有,畢竟不是自己的車,你不能停著然後在裡面放東西當行動置物櫃,再來就是計時消費只適合 A 點到 B 點的移動,中間如果想停下來看看風景或是買買飲料吃吃東西,都會有一點點不方便。另外 GoShare 以外的車不能換電池,租到沒電的車還得去找有電的車來換這點也是常常令我感到頭痛。


說到置物空間,這也是一個很艱難的事情。有時候要出門辦不少事情,或是行程比較多就會大包小包的。若不是自己開車騎車,就要找置物空間。雖說很多捷運站跟地下街都有提供置物櫃,但有時想要工作晚一點,這些置物櫃就不一定能夠提供服務到這麼晚。目前有到半夜的置物櫃在台北西門比較多,其他地方的話我目前還沒有看到有哪裡比較多戶外的置物櫃,可以提供 24H 服務的。


生活自律


獨立工作者跟一般上班族相比最難的就是自律的部分。這方面如果你恰巧不是很擅長也沒關係,並不是沒有訓練方式。


首先要先找到自己最舒服的作息時間,像是我自己的話中午之前是不可能工作的。我也經歷過朝九晚五的上班族生活,但無論過了多久,即使我早上是自然醒來沒有靠鬧鐘,中午之前大腦就是無法開機。即使喝咖啡跟提神飲料也沒有用。曾經我有花時間研究這到底是從什麼時候開始的,結果發現其實我從小就這樣,小時候睡 12H 到學校,早上的課依然是打瞌睡到爆,所以也許我就是個適合在過午後甚至深夜工作的人。


當然找到這個規律之後,能夠發揮產能的時候你還是要發揮一下產能,不要把那段時間用來追番會比較好一些。


再來就是有睡眠障礙的話盡量找方式排除,這方面壓力通常是一大因素,能的話找心理諮商資源來解決會是一個不錯的方式。如果找了許多方式都沒有辦法解決的話也不要焦慮或灰心,因為這會讓睡眠障礙更加難以排除。既然睡不著那就工作吧,就運動吧。獨立工作的一大優勢是我們可以自己決定工作時間,不會因為今天沒睡好,明天還得同一時間去上班開會做決策。當然獨立工作也是會要跟別人約時間討論就是了,但比起每天固定時間上下班,儼然已經有彈性許多。


忙碌時也要記得每天留時間給自己,這個跟「愛自己」說起來抽象但也是可以有一些具體的行動,清潔生活或工作環境、冥想、佈置自己的空間都是留時間給自己的很好方式。以前我總覺得留時間給自己就是看看一些喜歡的影片,打打一些自己喜歡的電玩,總而言之就是一些無所事事的活動,但後來才發現生活可以不只是這樣子。當然如果只是作一些懶散的事情可以讓你完全放鬆而不會累積壓力的話,就不需要一定得冥想什麼的,這點完全是看個人的狀況。


對於那些常常會莫名感受到焦慮的人,去看一下 GTD 或 Ikigai 之類的書籍可能會對自己現在該怎麼取捨事情有很大的幫助。莫名的焦慮跟能夠說出原因的焦慮不太一樣,這種焦慮通常是來自己價值的不確定性,身為獨立工作者一定要很清楚自我價值,跟自己的願景。否則很容易忙了很多年多頭空之外,還沒辦法留下一些累積的力量繼續往前,那樣子的空虛感會更加強烈。


而講到愛自己,忙碌的工作還是需要休假的。定期安排旅遊、給自己禮物,都能讓自己有持續向前的動力。過去我對自己很小氣,幾乎沒有買過禮物給自己,我所消費的東西,都是我「不得不去花費」的。後來才發現自己這樣小氣的結果是讓自己少了很多機會,例如不給自己買機車,少了去很多未知地方的機會;不給自己買單眼,就也少了紀錄很多美好時光的機會。


獨立工作者常常需要自己作決策,而那種長達幾年的專案更是容易為自己帶來挫折感。這時候設定期限就顯得相當重要。階段性目標若是沒能在期限內達成,就要問問自己究竟適不適合這個領域,適不適合獨立工作。可以給自己機會,但不能超過兩次。時間到了,就要接受事實,好好承擔自己做的決定,好好整理這段時間學習的東西,期待下次再出發。


雖說我魯我驕傲,但會投入獨立工作或製作的我們,鐵定還是希望蹲了幾年能夠跳一發高的啦。這篇雖然只是自己這段時間的流水帳式的生活經驗分享,我至今也還沒能跳起來,還是希望能對誰有一些幫助,那也就不枉我花了蠻多時間蹲在這邊啦。


從無到有的遊戲開發心得 - 這兩年獨立工作的流程檢討

製作遊戲是學習軟體開發以來一直很想做的事情,但在實際製作的過程中發現了很多自己缺少的思考能力,也帶來了很多挫敗感。2020 年 5 月初我給了自己一個 40 個小時的工作期限,在這之前要將一款遊戲從無到有生出來,並且事後要檢討作業流程,紀錄一下開發作業遇到的問題以及解決方式,而這段時間開發的成品是一款叫做「Unstable Rider」的手機遊戲(尚未上架)。


Unstable Rider - 最有趣(?)的夏日騎車遊戲

這並不是一個成功經驗


首先必須要強調的是,這份文件比較偏向於我個人開發的紀錄,主要是試著以客觀的角度全面觀察,在一個極限條件之下,自己能將遊戲開發進行到什麼程度,我手上擁有那些籌碼?如果想要更加快速,我還需要什麼資源?


對於有興趣但沒有經驗的人而言,或者是已經開發一兩年卻沒有成品的製作者,也許還是一份可以參考的資料。


我自己是軟體工程背景的開發者,對我而言軟體方面的資源是最充足的,因此必須要著眼在其他領域的部分,例如企劃、執行方法、時間管理,以及行銷。


每個人、每個團隊所適用的方式以及所遭遇到的問題可以說是完全不同,這也是產品製作最難、最有趣的部分,一旦你披荊斬棘找到了屬於自己的製作流程,那麼就誰也搶不走,以後無論遇到什麼問題,都可以最快的速度開始執行。


善用輔助製作的工具


遊戲必須是一個結構完整的小世界,即使是簡單的遊戲也需要一個完整的系統,除了要幫自己規劃目標,也要幫玩家規劃目標,從遊戲開始、遊玩過程到結束,至少要有一個循環的完整體驗。


因此遊戲製作的本質上是多個管理、決策問題的總和,而一個人要完成這些需要一些簡單好用的工具來輔助,接下來就介紹一下我在這次專案中使用比較多的一些工具來分享一下。


內容計畫


要在沒有任何計畫的情況下開始執行通常不會是一件正確的事情,但太多的計畫也會使人裹足不前,這中間該怎麼取捨確實是一件困難的事情。這時候分階段的計畫變得相當重要,先推薦一下一項線上工具 Milanote,依照個人使用習慣不同,可以有不同的使用方式,我主要是拿他來作隨手筆記以及決策紀錄,有興趣使用的人可以使用我的 Milanote 推薦連結 來註冊。


Marketing Plan Template, within the Milanote app
Milanote 是一個不錯的專案看板工具,如果你習慣使用 Trello 也可以。

以 Unstable Rider 的開發來說,我使用了畫面導向的計畫方式,也就是「從畫面來切割功能」,以最少的畫面來說,就是主畫面、遊戲介面,跟一個管理場景切換與載入的功能性場景。


程式開發


以遊戲製作來說選用一個好用的 Game Framework 可以省不少事,尤其是當你需要製作跨平台(例如手機 + 桌機)的遊戲,可以省下大量開發時間。主流的 Unreal Engine 跟 Unity 都很強大,但學習門檻也會相對高一些,Godot 的功能雖少,但應付簡單的遊戲開發已經足夠。


以一個軟體工程師而言,評估各項工具是否適合自己使用會相對輕鬆一些,但對非軟體工程背景的開發者而言則不是那麼容易,這時候多去看看別人的說法會是一個比較有效率的作法。如果看了許多分析還是不太知道該選擇哪一項工具時,選擇最多人用的則絕對不會有錯。並不是最多人用的東西最穩定,而是你遇到問題時,至少都找得到人問,找得到人解答。


我自己之所以不使用 Unity 的原因主要是我的筆電不太夠力,Godot 目前的發展相對於 2.x 版本也穩定許多,Unity 則是正好在 Render Pipeline 轉換的過渡期,許多功能相對有一些穩定度的問題,會需要時間作 workaround。


至於如果需要各項 Game Framework 的使用教學,這邊可以推薦幾個頻道(大部分都是英文):



繪圖


3D 繪圖軟體連業界都開始傾向使用 Blender,他好用又免費,對於窮困的獨立製作者而言,基本上沒有什麼其他選擇。兩年多前有學習了一些 Blender 的操作,臨摹三視圖做一些 Low Poly 的模型還算可行,不過光想要去找圖庫來慢慢描可能會花不少時間,我上次操作 Blender 也已經是一年前的事,只記得 G 是 grab,E 是 extrude,其他搞不好都忘光光,於是考慮了一下決定上 TurboSquid 找現成素材。


TurboSquid 上有很多質量不低的免費 3D 素材,只需要在使用時標示作者即可。

其他不錯的素材網站有 SketchFabcgtrader 等等。看到好素材不要吝嗇花錢,有時候一個高品質的 3D model 甚至省一餐就能買到,相當超值,而且 3D model 的重複使用性強,不用執著於一定要使用免費素材。


數學模型


遊戲的行進過程中常常需要建立數學模型,例如等級提升需要多少經驗值?難度曲線是線性成長或是曲線?預計讓玩家遊玩的時間上限?這些數字能夠幫助開發者在決定遊戲難度時,用更精確的方式去量化遊戲進行到某個時間點時的難度。


Unstable Rider 隨著時間玩家騎乘的距離越遠,有陰影可遮的機會將會越來越少,這部分也是靠 Desmos 這個網站簡單畫了幾個函數來達成。


Desmos 能夠繪製並儲存數學函數圖

音樂


本來打算自己做一段類似頭文字 D 的那種 Upbeat Eurodance 音樂,但因為取捨某些功能花了太多時間,導致我只能用之前的作品來放。


音樂資源可以到 GameDevMarket 去找。


音效


從音效庫搜尋想要的音效其實比想像中費時許多,因為音樂跟音效都不像圖案那樣可以一眼瀏覽很多資訊,必須一個一個去聽去過濾。時間有限的情況下,我使用了 BFXR 這個工具來製作簡單的音效,日後要作精緻一點再代換聲音檔即可。


簡易的遊戲音效合成器軟體 BFXR

挑戰與解決


功能取捨


在 Milanote 上寫下對遊戲的想法後,發現需要解決跟取捨的問題實在太多,而作決策時要考量的面向太廣的話,會增加決策難度以及拉長開發時間,因此我選擇了一個最優先目標:最小化開發時間。有了這個目標之後,本來打算自己刻 3D 模型、作配樂的計畫立刻無法實現。但如果不這麼做的話,恐怕半年後都還不會有 prototype 可以玩吧。(笑)


另外為了進一步縮短時間,我給自己設下了「每個功能開發或作決定不能超過 2hrs」的限制,當然這個限制有點籠統,不過在明顯覺得思考某個問題過久之後,就可以提醒自己還有這項限制的存在。在做每個功能跟嘗試之前紀錄一下開始的時間點也是一個很好的方式,幫助自己釐清是否在不必要的功能上花費了太多時間。


遊戲模式


首先如果你沒有遊戲企劃的經驗,建議可以多看一些 Game Jam 的影片,了解別人創造遊戲的發想過程。慢慢建立一些「怎樣的遊戲比較好做」的觀念,這樣才不會不小心選了一個不好起步的路線前進。


Unstable Rider 一開始的發想是在某個大熱天騎車時,發現自己常常需要躲在陰影底下遮陽,沿途就想了一下遊戲該怎麼進行:「玩家以時限內騎車最遠距離為目標,但被太陽曬太久會曬死。」成了最核心的系統概念,為了增加遊戲有趣的非現實成分,我特別多加了「曬到太熱時會有爆發性的加速」這個設計,當然最初的構想還有交通號誌、回血機制、延時機制等等,但開發途中都因為想在時限內完成而通通捨棄了,日後的版本再加上去吧。



工作細節與那些先被捨棄了的功能


許多決策問題中,遇到第一個卡關的是:遊戲畫面應該要是直式還是橫式?這看似簡單但實際上不是一個容易的決定,當遊戲打算要出在手機上時,就要考慮到手機基本上還是直的拿比較好拿,如果要設計讓人在大眾運輸上通勤時可以打發時間,那麼一手拉著拉環的情況下就無法操作橫式遊戲了;橫式的遊戲則是適合在玩家已經穩定坐在一個地方,可以專心投入的情況下。這會依照企劃者對於這款遊戲定位為休閒向還是競技向而有所不同。Unstable Rider 在極其有限的開發時間以及 2hrs 的單題思考限制下,決定了休閒向的主要路線。


再來是陰影角度。本來打算樹的陰影隨著日照時間而有所不同,這方面的程式其實也不會太難寫,只要太陽光的角度跟玩家身上的 RayCast Detection 一直是相反的向量就可以了,問題在於「視覺」的部分,這個 3D 遊戲畢竟不是使用 VR 來操作,人眼對於虛擬的 3D 景象,要抓距離是很困難的一件事情,而以這個遊戲被定位為休閒向的前提之下,開發到 2hrs 就立刻進入決策階段並且將這個功能捨棄掉了。


還有交通號誌,這是一個不算難寫,但卻嚴重影響遊戲體驗的一個功能,本來這個功能是核心概念,玩家必須要在紅燈前找地方躲,以免被曬死,但紅燈一多變得整場遊戲需要玩家操作的部分變得很少,反而花了很多時間在等紅燈,這個玩起來有一點荒謬,因此後來還是先移除了這項功能,日後再加入囉。


畫面特效上,本來打算除了 Particles 的應用之外,還想順便寫一些之前學的 Shaders 來增加豐富度,不過 2D Shaders 跟 3D Shaders 還是有一些不同,為了節省時間也是先打入冷宮了。


至於廣告,一方面是因為自己實在也是很討厭遊戲遊玩中一直有一些低質量的廣告插入,一方面是之前去 SensorTower 查了一些資料,發現有些沒有廣告的獨立遊戲營收也是不低,主要還是消費模式要建立起來,因此最後還是決定不放廣告。


另外,雖然決定用現成素材而不自己刻,但途中發現了 3D 素材網站有些共同的奇怪問題,例如很多作者附了 .obj 檔卻沒有附 .mtl,這就跟只給 .htm 沒給 .css 檔案一樣意思,或是附了一些 .f3d 這種 closed source 的格式,我花了蠻多時間才找到一款 CAD Exchanger 可以無痛轉檔,這應該是開發過程中意料之外多花時間的部分了。


從無到有的迷思


兩年前我有開了一個遊戲專案,堅持不使用別人的素材、程式、音樂,不參考同類型遊戲的作法,且不與人討論,從頭到尾自己想自己做。


我不認為這個方向是錯誤的,畢竟這個專案對我自己而言有一件獨特的意義,但兩年後的現在沒有產品產出也是事實,在有限的時間以及自己能力不足的情況下,也只能選擇用一些手段來妥協。不過製作的過程中想到,我使用 Game Framework 基本上就已經也是使用了別人的程式,因此陷入了一些很困難的邏輯思考。最後我所能想到的折衷方式就是另起一個專案捨棄這些堅持,但原來的專案還是繼續並延長開發時間,畢竟此刻我相當需要一些自己的作品集,而不是一個永遠無法完成的(自以為的)大作。


後職業倦怠症候群


雖然現在與職業倦怠可以和平共處了,但適當的休息以及工作壓力釋放還是相當重要的。工作一陣子後常常大腦會習慣性的放空,會有點無法判斷接下來要作什麼事情比較合乎效益。簡單來說就是「開發者的大腦」跟「管理者的大腦」沒有辦法順利切換,這種情況也許使用番茄鐘工作法會是一個不錯的方式,穿插開發工作與決策工作,用鬧鐘定時提醒自己在不同的角色中切換,以免陷入一個問題花了太久時間。但番茄鐘我一直沒有找到一個合適的輔助工具來使用,這方面我會再找找能夠與行事曆跟 ToDo List 完美整合的工具。


另外釋放工作壓力的方式,我選擇了一個最偷懶的方式:喝氣泡酒。


好啦,其實還有玩了一些遊戲,畢竟我這人還是蠻喜歡 hard fun 的。前兩個月開始我規定自己一週要玩三個新的遊戲,並嘗試在自己覺得不錯的遊戲上破關,在這邊順便推薦一些這幾年覺得不錯的遊戲:


  • 單人動作:Celeste(蔚藍)
    2018 年的獨立遊戲大作,IGN 滿分神作就不用多說了吧。
  • 益智休閒:Kami 2
    這是我說的那款沒有廣告但收益卻不錯的休閒小品遊戲,畫面非常精緻,填色遊戲方式雖然並非獨特但因為動畫展示方式而給人耳目一新的感覺。
  • 多人合作:Arena of Valor(傳說對決)
    DOTA 類型遊戲一場需要花費較多時間,而 AoV 這款遊戲針對在手機上遊玩而簡化了許多複雜的遊戲機制,門檻相對也降低很多,我自從玩了這款遊戲之後也變得比較能看懂多人塔防競技比賽那些微操作以及控線入野等戰略決策,由於單排需要面對各式各樣不同的人,且常常玩輔助跟打野位,連 EQ 都變得很高,畢竟要跟討厭的人一起團隊合作從來就不是一件簡單的事情呢。
  • 卡牌遊戲:Slay the Spire(殺戮尖塔)
    是需要耗腦的回合制 PVE 遊戲,每一局都是獨立的,可隨著遊戲進行解鎖更多遊戲內容,但原則上不需要練等,每天玩個一兩局已經相當足夠,想要動腦玩一些策略但又不喜歡過於龐大的戰略型遊戲,這款會是不錯的選擇。

最後 5 小時


途中取捨了很多功能,不過開發階段還是來到了尾聲。最後 5 小時,花了蠻多時間在瑣事上,例如產 AppIcon 以及上架 App Store 所需的螢幕截圖等等。這方面之前雖然也是有經驗,但再操作一次還是時時刻刻都想罵髒話。臨時抱佛腳找了一些產圖的網站但要不是都要支付高額訂閱費用,就是功能不齊全。


好不容易把該填的資料填完準備送審,才想到還有行銷這回事(崩潰)。當然這種只花幾天開發的小糞 Game 要吸引人來玩本來就是一件不太可能的事情,因此行銷這方面也是簡單成立個粉專,轉貼自己的廢文這樣就結束了,算是有點虎頭蛇尾。


可以再做更精緻的部分


如果要我選擇的話,我希望這個時限內做出的遊戲至少能再加入這些元素:


過場動畫


主畫面跟遊戲畫面之間目前是沒有動畫的,即使場景載入的程式不是寫得很順利,但基本的過場動畫還是不要偷懶比較好,現在看起來覺得這讓遊戲的完整度缺了一些,蠻可惜的。


延長遊戲時間


基本上雖然這是一款休閒向遊戲,我還是希望玩家能有機會在單指操作且熟練的情況下,可以秀一下操作。目前想到的方式是用手指向上滑動翹孤輪吃補包的方式來回血或延時,讓玩家有機會可以追求極致高分,這方面還在構想數學模型中。


一些ㄎ一ㄤㄎ一ㄤ的東西


「遊戲性不如其他作品,那麼至少畫面要有趣吧。」在最後處理行政問題的那五小時中我才想到這件事情,不過 3D Shader 完全沒有好好花時間寫過,要現在寫,臣妾作不掉啊!


於是到最後在主角被太陽曬到開始扣血的時候,讓牠以不合理的角度旋轉了一下,這個程式基本上只寫不到 8 行,不過讓整個畫面總算看起來有了一點靈魂,這是之前沒有好好考慮到的事情,以後還是多花點時間優先考慮一下好了。


大麻趕快合法化啦(咦)。


然後


App Store 送審至今已經被退了好幾次,網路上查了一下發現是沒有企業憑證的開發者本來初次送審就會比較困難,大概是沒辦法趕上預定的上架日期。這個計畫算是失敗啦~


不過這也沒關係,因為我總算是從三年前的泥沼中踏出了一小步,這份紀錄就當是一份給自己的結業證書,這個部落格會不會也從此跟著 Unstable Project 從廢文平台畢業了呢?齁齁。


Unstable Rider 遊戲的開發還是會繼續,但計畫上我必須要開始下一個小遊戲的製作了,下一次希望可以順利上架呀。


修復在 Google Kubernetes Engine 使用 Ingress + Rails force_ssl 會造成的 Error 502

由於 Ingress 目前還沒有支援直接 rewrite to HTTPS 的功能,目前只能在 App 層透過 Rails 在 production.rb 裡面設定 config.force_ssl = true 來讓瀏覽器轉向 HTTPS。然而將設定放上後,service 將會正常個一陣子,過不久後變成這個畫面。

然而 kubectl describe pod/POD_NAME 跟 kubectl logs POD_NAME 都沒有異狀,甚至 kubectl exec -it POD_NAME bash 去裡面 curl http://127.0.0.1:3000/ 也都是沒問題的,到底是什麼原因造成的呢?

到 GKE 的 dashboard 看,發現服務裡面的 ingress 項目中 ingress.kubernetes.io/backends 的 annotation 顯示為「UNHEALTHY」。查了一下發現原來 Ingress 在建立時若 deployment 沒有自己的 livenessProbe 的話,就會預設自己建立一個用 HTTP GET / 來判斷服務是否健康的服務,這個自動被建立的項目可以從「Google Computer Engine」裡面的「健康狀態
檢查 」看到,這個自動建立的項目有個問題是,當 GET / 時只要 HTTP 回傳狀態不是 200,都一律視為「UNHEALTHY」,而 Rails 的 force_ssl 選項會讓 HTTP 的連線都一律被回傳 HTTP 302(redirect to HTTPS://xxx),這時 Load Balancer 就會以為這個 Web app 有問題,而不把流量導到那邊去,所有節點都回傳 UNHEALTHY 時,這個網址也就爆了。

當然直接去修改健康狀態檢查也可以,不過能的話當然還是希望在 kubectl 佈署時就能搞定這一切對吧?

加上 livenessProbe 等敘述

在佈署時將 livenessProbe 與 readinessProbe 可以讓 Load Balancer 知道這個 pod 是否已經就緒,跟運行狀態是否良好。

  
apiVersion: apps/v1
kind: Deployment
metadata:
name: someapp
spec:
selector:
matchLabels:
app: someapp
template:
metadata:
labels:
app: someapp
spec:
containers:
- name: someapp
image: someapp/someapp:latest
imagePullPolicy: Always
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /healthcheck
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthcheck
port: 3000
initialDelaySeconds: 10
periodSeconds: 10

Rails 方面的 ssl_options 設定

另外這個 /healthcheck 也必須在 Rails config 裡面做些設定,讓這個網址在被 query 時不會被 redirect to HTTPS(此方法可能只適用 Rails >=5.2 的版本):

 
config.force_ssl = true # 或者較有彈性的 ENV[“FORCE_SSL”].present?
config.ssl_options = {
redirect: {
exclude: -> request { request.path =~ /healthcheck/ }
}
}

至於這個 /healthcheck 就要另外到 config/route.rb 裡面去另外開一個 route 給他了。懶得開的話,也可以使用某個 asset(例如 /404.html),並在 deploy 時的 env 記得加上 RAILS_SERVE_STATIC_FILES=true

deploy apply 了這個加了 livenessProbe 的設定後,記得重新建立一次 Ingress,如此一來新的 Ingress 才會自動使用這個健康狀態檢查的參數,而重新建立 Ingress 後可能會需要 10~20 分鐘左右的佈署時間,(尤其如果有使用 cert-manager 的話)。

以上是純 Rails 與 force_ssl 的會遇到的一些小麻煩,本來只是想偷懶不多用一層 nginx 來當 Rails proxy,結果反而好像弄得更麻煩了啊(菸)。

體制與挑戰:我的微反抗紀事

即使常常會覺得自己是個無所事事的人(特別是這十年),
但最近這兩年在審視自我、整理過去時,
意外發現十年間我還是有前進不少,
且更加確定了自己從小一直以來喜歡扮演的角色,
跟真正的個性。


輸贏之外的一段往事


大學時代比較多朋友是透過校內歌唱比賽認識我的,
我並沒有好的歌喉,音準更是糟糕到不行,
以一個表演者而言,我甚至不知道怎麼選擇適合舞台的衣服。
但我似乎知道如何在舞台上製造效果,
也知道觀眾看了整場無聊的比賽,想要獲得什麼。


印象最深刻是某屆比賽,竟然全場都唱抒情歌,
而且有三個人唱同一首歌,溫嵐的「傻瓜」。
即使有人真的唱得不錯,
但當時我在觀眾席聽到第三首時簡直像坐在主機板上難受,
我喜愛音樂,但我愛的是音樂的多元,
而不是整整兩三個小時的流行抒情 Ballad。


而評審當時也有請線上的製作人來,
如果我是製作人,聽完整場抒情歌,
我可能會只想趕快評一評趕快回家,
認為這是一個無聊,很難找到樂趣的地方。


於是有次我參加預賽時盡量認真地展現了其實並不存在的歌喉,
總算勉強擠進決賽,
挑選練舞功當我的決賽曲目,
成為比賽過程唯一一位讓台下沸騰的參賽者(自己說)。
其實在台上我是相當尷尬的,我沒有渾然天成的表演天分,
沒有經過任何歌唱與舞蹈訓練的我也體會到唱跳歌手有多難當,
我唱不到第二 part 就快斷氣,最後總算是勉強完成整段表演,
事後還有人給我看影片,我看了真的超想死。 😂
可是當時評審賽後一致同意特別開了一個獎項給我,給了我 500 元,
沒有實體獎盃,錢也少到在學校周邊吃個兩三天就沒了,
頒獎給我的評審沒有用麥克風跟我講了一句「你的想法很棒,表演就該這樣」,
至今對我而言都仍意義重大。


「有人看見我對這無聊的世界所做的小小反抗了。」


挑戰、勇氣與天份


講上述那段往事不是想表達我當時有多風光,
或是對於自己身處的團體有多大影響的人物。
我畢竟是個丑角,
能夠給大家的就只是一兩場秀,
多數人笑完離場後從此與你的人生無關,
而我期望的是能給底下幾個觀眾一些小小的啟發或靈感,
但也無從驗證當時是否有人能夠接收到。


回想起來從小到大類似的往事很多,
就慢慢確定了自己在社會中扮演的身分——體制的挑戰者。
也就是表面上都是順從著規則在走的人,
但內心一直想要作一些企圖顛覆體制的事情。


而對我而言,演出只是一個破壞體制與吸引注意的手段,
並非一定要執行的,
在演出的經驗慢慢累積之下,自己的勇氣也慢慢提升了不少,
讓我在沒有天份的情況下,對突發狀況多少有了些把握。


實際上我連言語的反應都很慢,
就連日常的對話很多時候都是先在腦內預演過才說出口的。
我也常在想「為什麼不交給其他人就好了呢」,
上台後才發現能夠為這樣的事情努力真好,
台上的演出者是否也都有過這樣的為難呢?


自由的必要


我的人生宗旨一直都是不想惹麻煩事,
但對我而言更糟糕的,是一成不變的那種無聊。


挑戰、破壞體制並不是代表體制不重要,需要被毀滅,
而正是因為有了體制,才有了破壞。
體制因為不斷被挑戰,才能持續更新與保障了公平、健全,
對於挑戰體制的人也該有使其破壞常規的自由,
這些的存在才使得世界變得有趣。


確定了自己相當享受這樣的角色與過程後,
從今以後也會持續進行小小的挑戰吧,
台灣不知道還能自由多久,
希望自己能永遠這樣調皮下去。


繼續臥底,持續破壞。


Peace


職業倦怠.續

28184251678_5e738485de_n之前寫了篇 職業倦怠
時隔八年多才出續集,真是個拖稿很久的爛故事啊。

直到最近我才算漸漸離開資訊行業,
理由也跟之前所講的相同,
畢竟賺錢要靠這個。

這幾年正職工作跟接案大多都與 Web 相關,
也是有寫過遊戲類、系統工具類,
也摸了一點前幾年最夯的 App。

途中唸了音樂的研究所,也組了樂團,
直到與教練樂團的主唱練了一次團,
才發現自己半調子的心態,
也許會侮辱了真正把全部人生都賭在音樂上的人。

於是我只想逃避,
結果就又逃回自己覺得舒適的資訊圈了。

現在對於資訊工作還是倦怠啦,
不過比較好的一點是,
比較知道令人疲倦與煩躁的點在哪了,
而不是只會唉說好累好累,也不知道累在哪。

Web 前端變全端


很多 case 都是一開始要你寫前端,
結果寫一寫變成前後端都要弄,
也許這跟需求訪談的技巧很有關係,
但比較常是大家試運行後發現了新的運作方式,
於是就只好兩者一起修正。

這種狀況,
即使會多給錢我其實都不太願意做,
畢竟這兩個領域的思維不一樣,
工作環境設定也會有所不同,
常常大腦得跟 configuration 一起做 context switch 真的超厭世的,
更別提除了開發腦之外,我還要再多顆 PM 腦來溝通。

變化太快的各方技術


以 Web 開發而言,主流語言或工具就好幾種,
而這些工具都沒幾天就會更新一次,
版本跳一號搞不好還沒辦法向下相容,
或冷不防來個全新的語法樣式。

對於開發中的軟體而言,
不斷的升級是一件痛苦的事情,
以 JS 而言,這幾年封裝的方式就變化很多。
(請參考 JavaScript 模組化七日談
還來不及學就可能換了一個新的流行方式,
即使 ES6 納入當作標準了,
瀏覽器可能也還來不及全面支援,
尤其像是企業內部會鎖版本的情況很多,
相容性也會跟著出問題。

當然這可以使用佈署的方式解決,
但畢竟還是牽涉到其他管理模式,
想要一人完成,不是一件容易的事。

總而言之


目前資訊相較於其他行業來說,
真的還是相當高薪的,
但隨著這份待遇而來的,
其實是被龐大技術能量追趕的壓力,
你也許天份很夠,不一定要賣肝,
但比別人多死幾億個腦細胞是鐵定的。

Debian Dropping Support For Older CPUs

2016 年這個消息出現後我思考了很久關於資訊的一些事情,
這個世界已經有 Ubuntu 這樣的 Linux dist 了,
Debian 需要也跟著快速發展嗎?
Debian 的更新較為緩慢,
本來應該代表著「使用者在用 Debian 時,不用擔心軟體更新過快,要一天到晚調整設定導致主機環境不安全的問題」才對。
可是諷刺的是,
Ubuntu 原本是基於 Debian 的開發版而衍伸的 OS,
現在使用 Ubuntu 的人很多,
反而一些從 Debian 帶來的冷門套件發生 bug 時,
排除得搞不好還比 Debian 快,
這樣 Debian 相對比較穩定且與低階系統相容的特性,
是否也漸漸消失了呢?

我希望軟體能持續更新到手上的機器在完全死亡之前,
不用一定要最新科技,跑 container 什麼的,
但上上網這要求不過份吧?

資訊行業現在就天天處於這種拉扯狀態中,
有人一輩子賭在某個平台或技術上,
這個平台一旦被某個併購消化掉,
損失的就是十年的青春啊,
雖說可以安慰自己「這十年我沒有白費,我學到超多理論的!」,
可是如果這個平台可以繼續再戰 10 年不是更好嗎?

總而言之,
資訊能力為自己所用是好事,
當作幫別人賺錢的技能,就顯得投資報酬率太低了。

斜槓青年 / 檔案整理大師

deskbook

右圖是我翻到古老的備份光碟中,2002 年個人網誌一側的插圖,應該是用 PhotoImpact 畫出來的,那毫無邏輯的光影真是銷魂啊。不過看著自己以前的網誌文字,就可以深深感受到自己還是改變了不少,也許說不上是成長吧,但至少是慢慢朝著目前覺得良好的方向在收斂。

~/music 或 ~/download/music


整理光碟時,對於以前檔案的排放方式還算滿意,大學時擺放檔案的邏輯變更了好幾次,不過至今還是有一件事情我始終無法釋懷:

「我該把下載的東西放在哪裡?」


身為從 2001 年開始資訊與音樂雙棲的斜槓青少年先鋒(自己說),音樂這個目錄是我永遠的痛。一般而言只要照 Album Artist 分類擺好就好,但許多專輯其實是多位作家共同推出的合輯,這時候硬是要分作家擺在不同的目錄中,就會顯得這個方式有點失敗。

後來我也試過將這個分類的結構打散,只針對不同專輯擺放就好,剩下的交給播放軟體去用 ID3 Tag 依不同需要去做分類,這個策略還蠻成功的,但是接下來面臨的問題就是:

「我自己創作的音樂作品要放在哪裡?」


很簡單啊,把自己的音樂作品當作一個合輯,都丟在裡面不就好了?可是這麼一來我的東西就會散落在各個目錄中,以 xdg 目錄結構規範就會像是這樣:

+-/
+-home
+-somebody
+-Pictures
+-mypics
–pic1.jpg
–pic2.jpg
–…
+-cute animals
+-pornstars
+-Musics
+-mymusic
–song1.mp3
–song2.mp3
–…
+-other artist 1
+-other artist 2

但在策略上,我們有時會無法整台硬碟一起備份(以前光碟容量不大,硬碟沒這麼便宜,當時經濟能力也無法說買就買,而以現在實際遇到的情況而言,是頻寬不足以全都上傳雲端),這時候要只備份自己的東西就會變得有點困難,這也是這次整理備份光碟時,遇到的最嚴重問題—―「確定有些重要的原創資料在備份時沒有被選到」。

講到這裡我都快哭了,人生最精華的創作歲月就這樣化為泡影啦!是還好那時候還蠻積極找知音的,知音們手上都還有我做的半成品,不過我去跟他們要那些幹嘛啦,真正的 artist 是不回頭看的(咦)

目前這個問題,我暫時打算未來將自己製作的專案檔都放在 Documents (My Documents / 我的文件)資料夾中,無論音樂還是程式專案都放在裡面,這樣我可以確保這裡面的東西是我原創,或是我參與其中的,這樣備份起來也會比較快速一點。

至於備份的問題,未來我打算再寫一篇「以第三方加密的方式存取主流雲端儲存空間」的文章,一股腦備份到雲端上,裡面可能會有些致命的隱私資訊啊,不得不防。

…………… 突然感覺我好無聊哦,這個話題也可以寫一篇文。 Orz

一些其它的碎唸


之所以會整理那些陳舊的資料,是起因於某天回頭一望,發現人生一團混亂。

不過幸好在長時間的調適之後,我的生活總算慢慢回到軌道上,雖然以我的個性而言,此生大概很難有什麼成就了,但也期許自己盡量別成為社會的負擔。

有趣的是,並非傑出青年的我,硬碟似乎比人生還要複雜混亂。在調適過程中我的主力硬碟剛好突然徹底地壞了,有鑑於裡面不只有我自己的隱私資料,實在沒辦法放心交給外人維修,最後用僅存的一些硬體知識嘗試修復,但最後仍沒能成功。

斷捨離可能也是個不錯的生活方式,不過我自己還是有點慶幸自己留下很多過去的痕跡,讓現在有一個跟過去對話的機會。我也藉機會整理出一些自己以往的缺陷,給從今以後的自己做參考:


  • 很習慣對事物做批判卻不先理解。

  • 太喜歡沉溺在遊玩與惡搞的專案,導致現在拿不出完整的作品集。


結果這篇弄得好像在 Medium 寫的勵志系列文啊。

在 Ubuntu 18.04 LTS 上跑 Xpra server

Xpra 是一個可以讓你在遠端機器跑視窗的好用工具,
我也是(後知後覺地)昨天才發現它。

有點長的前情提要


最近因為某些因素把 10 年前的 EeePC 拿出來讓他繼續工作,
沒想到連 Google Search 的搜尋結果都跑不太動啊……,
到底一個頁面裡面塞多少 JS code 才開心?
雖然也是有試過用 googler 在本機端以文字界面搜尋,
再用 links 開,但某些筆記環境的 web applications 這樣搞實在太克難啦,
只好到 Google Cloud Platform 租一台台灣的主機跑跑遠端桌面試試。

至於為什麼沒有使用 TightVNC,一方面他不支援 sound forwarding,
一方面覺得我從大學就看 VNC 看到現在也沒啥新東西出現,
就心血來潮去論壇尋找「VNC alternative remote desktop for Linux」,
忘記在哪一頁看到一堆人在說「你一定是沒用過 TightVNC 吧!(英文)Give it a try!」
心想馬德原貼文者就說不想再用 VNC 了你們是瞎了嗎?

於是就到 Wikipedia 的 Comparison of remote desktop software 去大海撈針(?),
我需要的遠端桌面環境有幾個條件:


  • 本身可以是個 service

  • 授權最好是開源系的(MIT / GPL / …)

  • 支援 Sound Forwarding(這樣要是要遠端看影片至少可以克難用一下啊)


最後撈出 Xpra 跟 X2Go。雖然 X2Go 超級好用且令人感動,
但不知道是什麼 bug 導致我用的第二天 X2Go 就再起不能,
一直卡在 x2gocleansessions 這個程序中,
連 reboot 大法也無法解決,最後去看 strace… 我看不懂啊皇上!
於是回來跟 Xpra 搏鬥。

(不過目前我也還沒找出讓 Ubuntu Xpra 支援 speaker forwarding 的方式就是了)

回到主題


而由於 Ubuntu 提供的 package 無法正確啟動 Xpra,
也沒有在 stderr 看出什麼有用的資訊,
在 /usr/lib/python2.7/dist-packages/ 裡面看 xpra 的 code,
跟目前 xpra trunk 的 code 有一些不同,
推測大概是主版本編號雖然一致,release version 卻還沒跟上吧,
只好下載原始碼自己 build 看看。

$ wget https://xpra.org/src/xpra-2.3.2.tar.xz
$ tar xJvf xpra-2.3.2.tar.xz
$ cd xpra-2.3.2.tar.xz

接下來看 README 的步驟安裝,
發現在 setup 時還是缺少了一些東西,
加上後來就算成功跑起來了還是不支援某些 encoding,
反覆測試跟看 log 之後,
整理出我安裝的 packages 是這些:

$ sudo apt install cython pkg-config libx11-dev libxtst-dev \
libxcomposite-dev libxkbfile-dev libxdamage-dev python-gobject-2-dev \
python-gtk2-dev xvfb cython libx264-dev libswscale-dev \
libavcodec-dev libvpx-dev libavformat-dev python-numpy python-avahi

# 再來是安裝與執行
$ ./setup.py install –home=install
$ ./install/bin/xpra start :100 #DISPLAY port 可自訂

Client 端用 apt 安裝一般的 xpra 後執行以下指令應該就可以 attach 了

$ xpra attach ssh:$USERNAME@$SERVER_IP:100 –encoding=h264

如果都已經啟動成功,可以在 server 端試試這個來看看 client 端會不會出現視窗

$ DISPLAY=:100 x-terminal-emulator & # 或其他 server 上有的 GUI 程式
裝完之後再裝自己喜歡的桌面環境跟瀏覽器,
就可以用 h.264 編碼爽爽的在 GCP 的 VM 上面用 YouTube 看影片了。
網路速度夠快的話延遲感還蠻低的,
(是說 X2Go 跑得順利的時候連聲音都有呢,GCP 太威啦!)

放在 AwesomeWM 裡面還可以跟 local 端的視窗並排,
以我目前的特殊狀況來說,這個功能我頗需要的。

遺憾的是


X2Go 除了不能像 Xpra 那樣把本機遠端視窗混合之外,
其實沒什麼可以挑剔的點,
剛灌好時跑得很順,顏色很鮮艷,速度也很快,壓縮也正常,影音表現也不賴,
還可以跟 VMWare 一樣 host / client 共享資料夾,
可是為什麼卡住的原因我要之後再來好好研究一下,
一開始以為是哪個 session 的資料存在硬碟中害他卡住,
後來家目錄的 .x2go 也移除了也是沒有解決,不懂到底是卡在哪……

另外就是 Xpra 雖然畫面可運行,可沒有辦法進行 speaker forwarding 的部份,
目前只確定是 server 端問題,
ps 有看到 Xpra 有順利 PulseAudio 程序,可是 log 卻看到一行

/run/user/1001/xpra/pulse-:100/pulse/native 沒有此一檔案

類似這樣的東西,可是我本機明明就有這個檔案啊 XD
好像是因為這樣沒有能啟動 Speaker Forwading 的功能,
這個也是待下回分曉了。