tag:blogger.com,1999:blog-20908506661335848552024-02-08T09:47:39.070+08:00This Site Has Moved to yodalee.meLee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.comBlogger247125tag:blogger.com,1999:blog-2090850666133584855.post-55715284856987940902020-08-20T00:08:00.003+08:002020-08-20T00:14:52.323+08:00從 blogger 轉換到靜態網頁生成看到這個標題大概就知道發生什麼事了……。<br />是的,經過十年 blogger 寫作,我終於決定要搬家了。<br /><br />故事是這樣子的,自從 2011 年起我就持續的有在寫 blog,那時候選的是 google 的 blogger 服務,剛看一下到目前為止總計 246 篇文章,算上這篇搬家文的話是 247(剛好是 13 * 19 ,<strike>可以當作 RSA 公鑰</strike>),大部分都是技術相關,也有部分是生活雜記跟書評等等。<br />總括來講 blogger 並不是不好用,相反的 blogger 提供可以用瀏覽器編輯內容、圖片上傳後自動歸檔到近乎無限的 google 雲端(這另一方面也是一個問題,刪除誤上傳的照片很麻煩)、google 在背後保證了網站的可及性和穩定度、手機與電腦都支援的瀏覽介面,如果你對網誌內容沒有什麼太嚴謹的要求,只是想要記錄一下生活貼貼遊記照片,blogger 已經大概有 80 分吧。<br /><div>特別是不要以今非古,以那個年代的網路環境,找免費空間架站還不是那麼容易的事,託管在 google/blogger 顯然是簡單很多的解法。<br /><br />blogger 的缺點,最後逼得個人跳槽的最大原因,我認為是:程式碼與凌亂的文章格式。<br />畢竟個人的 blog 從開始的定位就不是簡單記錄生活就算了,我非程式相關的文章大概 30 篇左右,程式相關內容佔了 80 % 以上,而程式碼是 blogger 最弱的一環,目前我寫作 blogger 的工作流程通常是這樣子:<br /><ol style="text-align: left;"><li>在 google doc 上面完成,我一個文件叫 write anything ,想寫什麼就全部往裡面倒。</li><li>整理成文章,把文章內容複製到 vim。</li><li>透過之前寫好的 <a href="https://yodalee.blogspot.com/2016/10/vim-script-blogger.html" target="_blank">blogger.vim</a> 將文章內容要劃重點、程式碼的部分加上 tag。</li><li>用 blogger html 編輯器,直接將全篇文章貼入。</li><li>在 blogger 一般編輯器,幫文章加上連結、圖片等。</li><li>來回檢視預覽和編輯器,修正所有顯示不如預期的地方。</li></ol>是的,我的文章從來不用 blogger 的編輯器,都是全部生好 html 再整個貼進去,程式碼的部分利用 <a href="https://github.com/googlearchive/code-prettify" target="_blank">pretty-print 套件</a>(剛剛看到它 archive 了…)為 <pre> 上色,或是 pretty-print 失效時加上 <div> 標籤加框,幾乎每次編輯 blog 都一定要去改動 html,搞得寫程式相關文章變得很痛苦。<br /><br />第二點也是我為何都用 html 編輯器的原因,透過 blogger 編輯器產生的內容格式非常不穩且難以維護,生出來的 html 簡直嘔吐物,很難再去手動修改 html ,但如上所述在寫文的時候去改動 html 幾乎是必要的,只能用編輯器的功能把所有格式都清除掉,那還不如一開始就寫 html 就好。<br /><br />特別是這幾年開始寫 rust 相關文章之後,上述問題整個變本加厲,因為 rust 特有的生命周期語法,會讓 pretty-print 的上色功能大噴射(它會以為 lifetime 'a 是字串開頭,其實根本不是…),每寫一篇 rust 的文章想跳槽的意念就深一層,最近的 amethyst 系列真的是壓垮 yodalee 的最後一根稻草,系列文完成前就決定跳槽,想要看完 amethyst 系列文的朋友只好說聲對不起了。<br /><br />這次跳槽的目標是轉換為 static site generator,其實這幾年來陸續都有朋友推薦這種寫作方式,但礙於懶得搬移舊文章一直沒有動手,推薦的工具也從早年的 <a href="https://jekyllrb.com/" target="_blank">Ruby/Jekyll</a> 到最近改推 <a href="https://gohugo.io/" target="_blank">Go/Hugo</a> 了;本來我是想說身為 rust 使用者,應該選擇 <a href="https://www.getzola.org/" target="_blank">Rust/Zola</a>(hail hydra…欸),只是後來發現 zola 的 theme 實在太少了,還是先用 hugo 撐撐場面。<br /><br />廢話不多說,讓我們來開始著手搬家吧。</div>Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com5tag:blogger.com,1999:blog-2090850666133584855.post-37044643207425751192020-08-09T23:52:00.001+08:002020-08-09T23:59:06.360+08:00使用 Amethyst Engine 實作小行星遊戲 - 9 UI現在讓我們來建 UI,我覺得建 UI 是目前掌握度比較低的部分,我也在想怎麼做才是對的。<br />
目標是加上一個計分用的文字:<br />
首先是 resource 的部分,建立 FontRes 來保存載入的字型檔,要建立文字的時候只要取用這個資源即可:<br />
<pre class="notranslate prettyprint lang-rust">pub struct FontRes {
pub font : FontHandle
}
impl FontRes {
pub fn initialize(world: &mut World) {
let font = world.read_resource::<Loader>().load(
"font/square.ttf",
TtfFormat,
(),
&world.read_resource(),
);
world.insert(
FontRes { font : font }
);
}
pub fn font(&self) -> FontHandle {
self.font.clone()
}
}
</pre>
<br />
這部分跟載入 sprite 沒有差很多,就不贅述;另外我們再實作一個 ScoreRes,用來儲存目前的分數跟儲存 UI 文字的 Entity。<br />
<pre class="notranslate prettyprint lang-rust">pub struct ScoreRes {
pub score: i32,
pub text: Entity,
}
impl ScoreRes {
pub fn initialize(world: &mut World) {
let font = world.read_resource::<FontRes>().font();
let score_transform = UiTransform::new(
"score".to_string(), Anchor::TopRight, Anchor::MiddleLeft,
-220., -60., 1., 200., 50.);
let text = world
.create_entity()
.with(score_transform)
.with(UiText::new(font, "0".to_string(), [0.,0.,0.,1.], 50.))
.build();
world.insert(ScoreRes {
score: 0,
text: text
});
}
}
</pre>
<br />
簡單來說 UI 的文字,自然也是一個 entity,裡面有兩個 components 分別是位移 UiTransform 跟 UiText;<a href="https://docs.amethyst.rs/master/amethyst_ui/struct.UiTransform.html" target="_blank">UiTransform</a> 會需要幾個參數:<br /><ul style="text-align: left;"><li>id :幫助辨識是哪個 Ui 元件。</li><li>
anchor、pivot:Ui 元素位在 parent 的哪個方位、位在自己的哪個方位,可以用九宮格的方式來指定。</li><li>
後面五個數字則是指定 x, y, z, width, height。</li></ul>
這裡我們把顯示擊落數的文字定在畫面右上角,x y 的位移值剛好補償它的寬度跟高度。<br />
UiText 需要帶入剛剛讀進來的字型,後面指定文字內容、顏色跟尺寸。<br />
<br />
要修改文字的話,我們稍微修改一下先前實作的 Collision System,要新增存取 ScoreRes 這個 resource,另外記得上面所說 UI 文字也是 entity,文字的資訊是保存在 UiText 這個 Component 裡面,所以要改文字,我們一併要存取 UiText 這個 Component:<br />
<pre class="notranslate">type SystemData = (
WriteExpect<'s, ScoreRes>
WriteStorage<'s, UiText>
)
</pre>
<pre class="notranslate prettyprint lang-rust">fn run(&mut self,
(mut scoretexts,
mut uitext): Self::SystemData) {
// change score
scoretexts.score = scoretexts.score + 1;
if let Some(text) = uitext.get_mut(scoretexts.text) {
text.text = scoretexts.score.to_string()
}
}
</pre>
先直接修改 resource 裡面儲存的 score 的值,再來是用 Component uitext 去取出 resource 裡記錄的 text entity,這樣拿出來的就是這個 entity 所含的 UiText Component,這時候才能去修改它的 text 屬性。<br />
上面這段 code 我放在處理碰撞的地方,只要有雷射砲跟小行星碰撞就會執行一次,真的要分得非常詳細,可以把這段移到獨立的系統中,比較不會亂掉。<div><br />
你可能會問,這樣不就…讓 ScoreRes 這個 resource 的實作內容給暴露出來了,不能把 UiText 跟 score 等等的好好好封裝到一個 struct 裡面,並公開介面如 setText 讓外部使用嗎?沒錯當初我也是想要這樣設計,只不過到目前為止都沒有成功過 (._.),目前只能將就一下用這樣難看的寫法,畢竟連官方的<a href="https://book.amethyst.rs/master/pong-tutorial/pong-tutorial-05.html#updating-the-scoreboard" target="_blank">範例</a>都是這樣教……如果有大大知道的話也請不吝賜教。<br />
<br />
</div>Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-41396747663104208592020-07-10T21:12:00.001+08:002020-08-09T23:25:35.165+08:00使用 Amethyst Engine 實作小行星遊戲 - 8 使用 ncollide2d 實作碰撞碰撞偵測應該也是許多遊戲內必要的元素之一,比如說我們的打小行星遊戲,就需要偵測雷射砲跟小行星的碰撞,以及小行星和太空船的碰撞。<br />
簡單一點的土砲法,是用 for loop 把小行星跟電射砲的座標收集起來,太近的兩者把 entity 刪掉就行了;但我們畢竟身為專業的遊戲設計(才怪),用土砲法就太遜了,這裡我們用同樣是 rust 寫的 <a href="https://ncollide.org/" target="_blank">ncollide2d 套件</a>來實作碰撞偵測。<br /><br />
因為 amethyst 內部包了一層 nalgebra 的關係,我們的 ncollide2d 用的版本必須是 0.21 版,我覺得這個問題挺…麻煩的,要自己去搜 amethyst 相依的 nalgebra 到底是哪個版本,然後對應回 ncollide2d 對應的版本。<br />
為了識別每一個物件的屬性,我們在各個 entity 上面都加上一個新的 component:Collider,內含一個 enum 屬性;在每個 entity 上面都要附上這個 component 作為識別。<br />
<pre class="notranslate prettyprint lang-rust">pub struct Collider {
pub typ: ColliderType
}
pub enum ColliderType {
Ship,
Bullet,
Asteroid,
}
</pre>
從我們之前的教學文學到的 rule of thumb:要改變行為,就是加一個系統。<br />
<pre class="notranslate prettyprint lang-rust">use ncollide2d::{
bounding_volume,
broad_phase::{DBVTBroadPhase, BroadPhase, BroadPhaseInterferenceHandler}
};
</pre>
引入 ncollide2d 相關的模組。<br />
<br />
<pre class="notranslate">#[derive(SystemDesc)]
pub struct CollisionSystem;
impl<'s> System<'s> for CollisionSystem {
type SystemData = (
Entities<'s>,
ReadStorage<'s, Collider>,
ReadStorage<'s, Ship>,
ReadStorage<'s, Transform>,
);
</pre>
這段就只是宣告一下 system,因為有生命周期上色很麻煩所以獨立出一段來。
<pre class="notranslate prettyprint lang-rust">fn run(&mut self,
(entities, colliders, ships, transforms): Self::SystemData) {
// collect collider
let mut broad_phase = DBVTBroadPhase::new(0f32);
let mut handler = BulletAsteroidHandler::new();
for (e, collider, _, transform) in (&entities, &colliders, !&ships, &transforms).join() {
let pos = transform.translation();
let pos = Isometry2::new(Vector2::new(pos.x, pos.y), zero());
let vol = bounding_volume::bounding_sphere( &Ball::new(5.0), &pos );
broad_phase.create_proxy(vol, (collider.typ, e));
}
broad_phase.update(&mut handler);
</pre>
新增一個 CollisionSystem,要動到有 Collider component 的 entity,讀位置需要 Transform。<br />
上面顯示了 ncollide2d 提供的 <a href="https://docs.rs/ncollide2d/0.21.0/ncollide2d/pipeline/broad_phase/struct.DBVTBroadPhase.html" target="_blank">DBVTBroadPhase</a> 的碰撞偵測,它會把輸入的資料依照空間位置分到樹狀的結構上,更有效率的去偵測碰撞。<br />
用 for loop 把所有非 ship 的 collider 取出來,從 transform 建立物體位置,再建立 ncollide2d 提供圓形 bounding_volume,這是 DBVTBroadPhase 偵測用的 key。<br />
我們用 (ColliderType, Entity) 作為 value,ColliderType 是用來識別碰撞的物體型別,我們只希望偵測子彈跟小行星的碰撞,忽略其他像小行星自己的碰撞;Entity 則是在碰撞發生的時候,能夠追溯到是哪個 entity 發生碰撞。<br />
最後只要呼叫 DBVTBroadPhase 的 update 並代入實作 <a href="https://docs.rs/ncollide2d/0.21.0/ncollide2d/pipeline/broad_phase/trait.BroadPhaseInterferenceHandler.html" target="_blank">BroadPhaseInterferenceHandler</a> 的 struct 即可。下面就來實作 handler:<div>
<pre class="notranslate prettyprint lang-rust">struct BulletAsteroidHandler {
collide_entity: Vec<Entity>,
}
impl BulletAsteroidHandler {
pub fn new() -> Self {
Self {
collide_entity: vec![],
}
}
}
type ColliderEntity = (ColliderType, Entity);<br />
</pre>
定義一下 type alias 寫起來比較方便:<br />
<pre class="notranslate prettyprint lang-rust">impl BroadPhaseInterferenceHandler<ColliderEntity> for BulletAsteroidHandler {
fn is_interference_allowed(&mut self, a: &ColliderEntity, b: &ColliderEntity) -> bool {
a.0 != b.0
}
fn interference_started(&mut self, a: &ColliderEntity, b: &ColliderEntity) {
self.collide_entity.push(a.1);
self.collide_entity.push(b.1);
}
fn interference_stopped(&mut self, _a: &ColliderEntity, _b: &ColliderEntity) {}
}
</pre>
is_interference_allowed 用來判斷這兩個物體能不能發生碰撞,這裡要求它們的 ColliderType 要不一樣;interference_started 則是碰撞發生時的處理,把兩個 entity 存起來;interference_stopped 處理碰撞結束的行為,留空就好<br /><br />
上面呼叫完 broad_phase.update(&mut handler) 之後,從 handler.collide_entity 就能拿到碰撞的 bullet 跟 asteroid,直接刪掉 entity 即可。<br />
<pre class="notranslate prettyprint lang-rust">for e in handler.collide_entity {
if let Err(e) = entities.delete(e) {
error!("Failed to destroy collide entity: {}", e)
}
}
</pre>
這篇其實非常偷懶了,目前至少有下面兩點可以改進:<br /><ul style="text-align: left;"><li>
不用在 system 裡面產生新的 DBVTBroadPhase 並重新插入 proxy,應該把碰撞偵測當成一個 resource,每次只要更新 DBVTBroadPhase 內記錄的位置,速度應該會比我們這樣從頭打造一個快。</li><li>
將 bounding_volume 存在 Collider 裡面,而不是每個東西都是單一尺寸的圓形,這樣也不用每次都重新生成新的 bounding_volume,從 Collider 裡面拿就可以了。</li></ul>
不過在我們這個小遊戲上還不需要在意這個,做完這一大步,現在遊戲已經有個可以玩的樣子了……雖然我立刻發現它難度有點太高了,我通常撐不到十秒,<strike>放不出 C8763</strike>,不過這只需要我們動一些參數,最後再來調整就好了。<br />
</div>Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-79978875861088150202020-07-05T21:23:00.002+08:002020-07-05T21:23:53.228+08:00使用 Amethyst Engine 實作小行星遊戲 - 7 亂數亂數在遊戲中也是個舉足輕重的腳角,少了亂數的遊戲就像沒加珍珠的奶茶(?,讓玩家食之無味;這章我們會加上亂數,以及產生小行星的 system。<br />
<br />
亂數直接用 rust 官方的 rand 模組,為了把它嵌入 ECS 裡面,可以觀察一下亂數模組會有什麼特性:大家使用一個亂數模組而不是大家都有一份,符合這個特性的就是 resource 了。<br />
在 resources.rs 加上一個新的 struct:<br />
<pre class="notranslate prettyprint lang-rust">pub struct RandomGen;
impl RandomGen {
pub fn next_f32(&self) -> f32 {
use rand::Rng;
rand::thread_rng().gen::<f32>()
}
}
</pre>
它把 rand 模組給包起來,在內部呼叫 thread_rng().gen 產生 f32 的亂數,在載入資源的時候我們一併插入這個 resource,AsteroidRes 請仿造 ShipRes 跟 BulletRes 寫一份,這裡應該可以再把三個資源共用的部分抽出來,不過因為是範例 project 我就沒這麼做:<br />
<div class="hl notranslate">
ShipRes::initialize(world);<br />
BulletRes::initialize(world);<br />
AsteroidRes::initialize(world);<br />
world.insert(RandomGen);<br />
</div>
ECS 的道理,要加上新的行為就是寫一個新的系統,我們把和生成小行星有關的設定都放在這個系統裡面(雷射槍的冷卻時間和太空船是綁在一起的,因此放在 Ship component 裡面):<br />
<pre class="notranslate prettyprint lang-rust">#[derive(SystemDesc)]
pub struct SpawnAsteroidSystem {
pub time_to_spawn: f32,
pub max_velocity: f32,
pub max_rotation: f32,
pub distance_to_ship: f32,
pub average_spawn_time: f32,
}
</pre>
實作上,entities、LazyUpdate 是產生新物件必備;Ship、Transform 用來取得船的位置,免得小行星直接出現在太空船的旁邊;AsteroidRes、RandomGen 則是需要的資源:<br />
<pre class="hl notranslate">impl<'s> System<'s> for SpawnAsteroidSystem {
type SystemData = (
Entities<'s>,
ReadStorage<'s, Ship>,
ReadStorage<'s, Transform>,
ReadExpect<'s, AsteroidRes>,
ReadExpect<'s, RandomGen>,
Read<'s, LazyUpdate>,
Read<'s, Time>,
);
</pre>
一開始就跟 ShipControlSystem 一樣,從 time.delta_seconds 取得秒數,去扣掉 system 的 time_to_spawn,一但低於零就會生成一顆小行星,並將 time_to_spawn 設定回 average_spawn_time。<br />
<pre class="notranslate prettyprint lang-rust">let mut create_point: Vector3<f32> = zero();
// generate creation point
loop {
create_point.x = rand.next_f32() * ARENA_WIDTH;
create_point.y = rand.next_f32() * ARENA_HEIGHT;
if (ship_translation-create_point).norm() > self.distance_to_ship {
break;
}
}
transform.set_translation_x(create_point.x);
transform.set_translation_y(create_point.y);
</pre>
生成點的位置我用了偷懶的方式,一般在 system 內最好不要有這種執行時間不確定的東西,如果一不小心 distance_to_ship 設太大,可能會讓 system 進到無窮迴圈把整個遊戲卡住,比較好的做法應該是從 distance_to_ship,亂數產生距離跟方位角就可以了。<br />
<pre class="notranslate prettyprint lang-rust">let e = entities.create();
lazy.insert(e, Asteroid {} );
lazy.insert(e, transform);
lazy.insert(e, physical);
lazy.insert(e, asteroidres.sprite_render());
</pre>
所有的 component 都準備好之後,一樣透過 LazyUpdate 把 component 塞進 entity 裡面即可,現在我們的遊戲應該有個樣子,平均每兩秒生成一顆小行星,按空白可以射出雷射。<br />
我們還沒實作碰撞,所以自然還沒有任何打爆小行星的行為,雷射打到小行星也只是無視的飛過去,下一章我們就進到遊戲除了亂數之外另一個必備成員:碰撞。<br />
<br />
Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-71482908723569486172020-07-03T23:29:00.000+08:002020-07-03T23:29:10.681+08:00使用 Amethyst Engine 實作小行星遊戲 - 6 刪除物體一般來說這種射小行星的遊戲,都會有一個莫名的設定,那就是太空船和小行星來到邊界的時候,會從螢幕的對面出現,就好像畫面是一個攤平的球體一樣。<br />
估且不論這個設定合不合理,我們就來實作一下:<br />
<br />
託 amethyst 之福,所謂實作就是:加一個新的 system,這裡叫它 BoundarySystem:<br />
<pre class="notranslate hl">
#[derive(SystemDesc)]
pub struct BoundarySystem;
impl<'s> System<'s> for BoundarySystem {
type SystemData = (
WriteStorage<'s, Transform>,
ReadStorage<'s, Physical>,
ReadStorage<'s, Bullet>,
Entities<'s>,
);
</pre>
這個 system 要改動的是所有含 Physical Component 的 entity 的 Transform 屬性,設定 Transform 為 Write;要刪除 entity 因此需要 Entities。<br />
<pre class="notranslate prettyprint lang-rust">
for (_, _, transform) in (&physicals, !&bullets, &mut transforms).join() {
let obj_x = transform.translation().x;
let obj_y = transform.translation().y;
if obj_x < 0.0 {
transform.set_translation_x(ARENA_WIDTH-0.5);
} else if obj_x > ARENA_WIDTH {
transform.set_translation_x(0.5);
}
if obj_y < 0.0 {
transform.set_translation_y(ARENA_HEIGHT-0.5);
} else if obj_y > ARENA_HEIGHT {
transform.set_translation_y(0.5);
}
}
</pre>
之所以要引入 bullets,是因為我們希望把含有 bullet component 一出畫面就直接消失,需要分出來特別對待。在 for loop join 的地方,可以列出一排 component,取出「包含所有列出 component 的 entity」,也可以用 Logical negation operator !,取出「不包含這個 component 的 entity」,就可以把 bullet 給排除在外。<br />
<pre class="notranslate prettyprint lang-rust">
for (e, _, transform) in (&*entities, &bullets, &mut transforms).join() {
let x = transform.translation().x;
let y = transform.translation().y;
if x < 0.0 || y < 0.0 || x > ARENA_WIDTH || y > ARENA_HEIGHT {
if let Err(e) = entities.delete(e) {
error!("Failed to destroy entity: {}", e)
}
continue;
}
}
</pre>
另外一個迴圈處理 bullet,這次我們用 &*entities 拿到對應的 entity,在超出螢幕範圍的時候,呼叫 entities.delete(e) 把 entity 給刪除。<br />
<br />
其實刪除 entity 就是如此簡單,單獨成一章實在有點不平衡XD。<br />
Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-37432723281689761052020-07-01T00:07:00.000+08:002020-07-01T00:07:29.523+08:00使用 Amethyst Engine 實作小行星遊戲 - 5 生成物體其實這章才是真的讓這堆教學跟官方 pong 不一樣的地方,在遊戲內生成 entity;官方的 pong 就是生出兩塊板子一顆球,球跑到場外就計分然後把球放回場中間,完全不會新增/刪除 entity。<br />
<br />
在這之前我們先做一點 refactor,之前我們在 states.rs 裡寫了一個函式:load_sprite_sheet 用來載入 sprite 資源,但這個資源沒向 world 註冊,在 system 裡面會無法使用。<br />
先來改善這點,把 Handle<SpriteSheet> 包進 struct,放到另一個 textures.rs 檔案裡:<br />
<pre class="notranslate prettyprint lang-rust">pub struct SpriteStore {
handle: Handle<SpriteSheet>
}
</pre>
這個 struct 提供兩個函式:<br /><ul style="text-align: left;"><li>
from_path:利用 load_sprite_sheet 取得 Handle<SpriteSheet>,建構 SpriteStore</li><li>
sprite_render:給定一個 frame id ,從 handle 建構出 SpriteRender。</li></ul>
另外一個新檔案則是 resources.rs:<br />
<pre class="notranslate prettyprint lang-rust">pub struct ShipRes {
pub sprite_store: SpriteStore,
}
impl ShipRes {
pub fn initialize(world: &mut World) {
let sprite_store = SpriteStore::from_path(world, "ship");
world.insert(
ShipRes { sprite_store : sprite_store }
);
}
pub fn sprite_render(&self) -> SpriteRender {
self.sprite_store.sprite_render(0)
}
}
</pre>
用一個 struct 再把 sprite_store 包起來,我在名字後綴 Res 表示 Resource,用來跟 component 的 ship 做區隔,不然到處都是 Ship 很麻煩。<br />
這個 struct initialize 拿到 SpriteStore 後,會呼叫 world.insert() 把自己這個資源註冊到 world 裡面,使用同樣的結構,我們可以另外生成資源 BulletRes 用來生成子彈。<br />
記得在產生 game data 的時候,呼叫資源 struct 的 initialize 函式。<br />
<br />
現在我們可以擴充我們的 ShipControlSystem 了:<br />
<pre class="notranslate hl">impl<'s> System<'s> for ShipControlSystem {
type SystemData = (
WriteStorage<'s, Physical>,
WriteStorage<'s, Ship>,
ReadStorage<'s, Transform>,
ReadExpect<'s, BulletRes>,
Entities<'s>,
Read<'s, LazyUpdate>,
Read<'s, InputHandler::<StringBindings>>,
Read<'s, Time>,
);
</pre>
擴充後的 SystemData 大幅增加取用的類別:<br /><ul style="text-align: left;"><li>BulletRes 是 Resource,因為沒有 default 我們使用 ReadExpect 來讀取。</li><li>Entities<'s> 要新增/刪除 entity 必要的</li><li><a href="https://docs.rs/amethyst/0.9.0/amethyst/ecs/prelude/struct.LazyUpdate.html" target="_blank">LazyUpdate</a> 是 amethyst 提供的一個方式,如果一條更新會動到很多的資源,可以先用 LazyUpdate 的方式記下來之後一次處理。</li><li>Time 跟 Ship 的寫入權限是要處理冷卻時間用的。</li></ul>下面是在 ShipControlSystem 和射出子彈有關的原始碼:<br />
<pre class="notranslate prettyprint lang-rust">fn run(&mut self,
(mut physicals,
mut ships,
transforms,
bullet_resources,
entities,
lazy,
input,
time): Self::SystemData) {
for (physical, ship, transform) in (&mut physicals, &mut ships, &transforms).join() {
let shoot = input.action_is_down("shoot").unwrap_or(false);
// handle shoot
if ship.reload_timer <= 0.0f32 {
if shoot {
ship.reload_timer = ship.time_to_reload;
let bullet_transform = transform.clone();
let velocity = transform.rotation() * Vector3::y() * 150f32;
let velocity = physical.velocity + Vector2::new(velocity.x, velocity.y);
let bullet_physical = Physical {
velocity: velocity,
max_velocity: 200f32,
rotation: 0f32,
};
let e = entities.create();
lazy.insert(e, Bullet {} );
lazy.insert(e, bullet_transform);
lazy.insert(e, bullet_physical);
lazy.insert(e, bullet_resources.sprite_render());
}
} else {
ship.reload_timer = (ship.reload_timer - delta).max(0.0f32);
}
}
}
</pre>
首先,在遊戲裡面會重複的只有 component,所以需要用 for loop 配 join 解開的也只有用 ReadStorage/WriteStorage 拿進來的 component,其他都不用。<br />
接著我們會去檢查 ship component 儲存的 reload_timer,依照 Time delta 減少,變為 0 就可以射擊,一但射擊了就會把 reload_timer 加一個冷卻時間。<br />
後面會用 ship 的 transform 算出子彈速度,產生子彈用的 physical component。<br />
產生 entity 其實很簡單,呼叫 entities.create() ,再用 LazyUpdate insert,往這個 entity 裡面塞 component。<br />
bullet_transform 跟 physical 都是我們剛產生的;resource 一定要先註冊過之後,才能像這樣用 ReadExpect 拿出來用,我們直接呼叫 sprite_render 函式,拿到可顯示的 SpriteRender component,一樣塞進去就行了。<br />
<br />
用 system 產生 entity 就介紹到這邊,其實沒有很難;大家應該更能感受到 ECS 系統的精神,Entity - 我們在螢幕上面看到的船 - 其實就是個空殼。<br /><ul style="text-align: left;"><li>
它為什麼可以顯示東西?我們幫他加上一個 SpriteRender component。</li><li>
它為什麼有速度可以旋轉?它有 physical component。</li><li>
要變化位置?加上 transform component ,讀取 physical 來更新它。</li><li>
要設定加速度的值?把加速度存在 ship component 裡。</li></ul>
沒錯,所有的東西都是 component 達成的,entity 一點用也沒有,隨之而來的就是彈性,還要加上碰撞?在需要碰撞的三個東西:船、小行星、雷射加上一個新的 Collider component ,再寫新的系統處理它就可以了,舊有的程式碼完全不需要改動,這大概是 ECS 系統帶來的最大好處了。<br />
<br />
Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-36566955096107265752020-06-29T00:48:00.004+08:002020-06-29T00:55:15.933+08:00使用 Amethyst Engine 實作小行星遊戲 - 4 移動物體上一章連接輸入的部分,我們完成了第一個 system 的設計,當然這個系統什麼事都沒做,這章我們就來把輸入接到真正的變化上。<br />
<br />
一開始讓我們在 component.rs 裡面新增一個 component physical:<br />
<pre class="notranslate prettyprint lang-rust">use amethyst::{
core::math::Vector2,
};
pub struct Physical {
// velocity, [vx, vy]
pub velocity: Vector2<f32>,
// maximum velocity (units / s)
pub max_velocity: f32,
// rotation (rad / s)
pub rotation: f32,
}
impl Component for Physical {
type Storage = DenseVecStorage<Self>;
}
</pre>
<br />
直覺上來想,可能會覺得我們之前產生的 Ship component 裡面應該要有 velocity 屬性,system 會讀取 velocity 去更新太空船的位置 - 也就是 transform。<br />
但是不對,因為畫面上會動的東西不止有 Ship,之後會新增的子彈跟小行星都是會動的,而實作<會動>都是共同的,寫在 Ship 內的性質無法共用,所以我們直接新增一個 physical component 記錄速度和旋轉量,想要動的東西只要加上 physical component 就行了,程式碼共用的部分可以很漂亮的抽出來。<div>這裡我們直接使用 nalgebra 的 vector2 來代表速度,amethyst 裡面有包一包 nalgebra 並且重命名為 core::math,版本是 0.19.0,這個在我們後面引入 ncollide2d 的時候會帶來麻煩,不過這裡就先用吧。<br />
<br />
state.rs 裡面,在 initialize_ship 函式幫產生的 entity 加上 Physical component:<br />
<pre class="notranslate prettyprint lang-rust">use crate::components::{Ship, Physical};
world
.create_entity()
.with(transform)
.with(sprite_render.clone())
.with(Ship::new())
.with(Physical {
velocity: zero(),
max_velocity: 10.0,
rotation: 0.0
})
.build();
</pre>
現在來修改 system,先新增一個操作 Physical component 的 system:<br />
<pre class="notranslate">#[derive(SystemDesc)]
pub struct PhysicalSystem;
impl<'s> System<'s> for PhysicalSystem {
type SystemData = (
ReadStorage<'s, Physical>,
WriteStorage<'s, Transform>,
Read<'s, Time>,
);
fn run(&mut self,
(physicals,
mut transforms,
time): Self::SystemData) {
let delta = time.delta_seconds();
for (physical, transform) in (&physicals, &mut transforms).join() {
let movement = physical.velocity * delta;
let rotation = physical.rotation * delta;
transform.prepend_translation(Vector3::new(movement.x, movement.y, 0.0));
transform.rotate_2d(rotation);
}
}
}
</pre>
每個系統都要定義 SystemData,也就是它需要存取哪些資源:無非就是讀寫 component、新增/刪除 entity 還有讀取 world 內存的 resource,PhysicalSystem 會需要去讀 Physical component、系統提供的 Time resource、寫入 transform component。<br />
在讀取和寫入有不同的方式,可以參考<a href="https://book.amethyst.rs/stable/concepts/system.html" target="_blank">文件 system 章節</a>,大致整理起來是:<br /><ul style="text-align: left;"><li>
Read<'s, Resource>、Write<'s, Resource>:取得唯讀/可讀寫的 Resource,這個是保證不會失敗的取得資源,如果失敗的話會直接給你一個 Default::default() 的版本。</li><li>
ReadExpect<'s, Resource>、WriteExpect<'s, Resource>:同上,但這個適用在沒有實作 Default::default() 的資源上。</li><li>
ReadStorage<'s, Component>、WriteStorage<'s, Component>:取得唯讀/可讀寫的 Component 參考。</li><li>
Entities<'s>:創造或刪除 entity 用。</li></ul>
實作 system 只要實作一個 run 函式,這裡有兩種寫法,一種如上面所示,在參數階段就把 SystemData 解開來;另一種則是在函數內解開:<br />
<pre class="notranslate prettyprint lang-rust">fn run(&mut self, data: Self::SystemData) {
let (physicals,
mut transforms,
time) = data;
// ...
}
</pre>
兩種對編譯器來說應該是一樣的,所以選一個喜歡的就可以了,但記得一個原則,一定要把每一行都分開寫,不要擠在一行:<br />
<pre class="notranslate prettyprint lang-rust">let (physicals, mut transforms, time) = data;
</pre>
這是因為我們的 system 是會長大的,哪天要加一個新的 component,直接加一行會比在一行裡面找到正確的位置還要簡單。<br />
<br />
從 SystemData 拿到的,會是這個遊戲裡「所有有這個 component 的資料」,這裡會收集到所有有 Physical component 的 entity;所以我們用 for loop ,搭配 join() 把資料展開來。<br />
只要是 component 展開這步幾乎是必要的,我個人是建議從 system data 裡解出來的資料,一律加 s 用複數,用 for 解出來再變單數,變數名詞選同一個,比較不會去考慮哪個是哪個。<br />
再來就是位移跟旋轉的實作,從 <a href="https://docs.amethyst.rs/stable/amethyst_core/timing/struct.Time.html" target="_blank">Time</a> 這個 resource 裡面,我們可以拿到和上一個 frame 之間的時間差,配合 physical 裡面記錄的速度和角速度算出位移量,並更新到 transform 就行了。<br /><br />
上一篇的 ShipControlSystem 也要修改,它會從 ship 定義的加速度算出速度的變化值,修改到 physical 速度內,這裡揭示了 for loop 配 join 的用法,從 ReadStorage<'s, Physical> 拿到的,是所有「有 physical component」,只想要改 ship,只要把 physicals、ships 一起放進 join 裡面,就會只拿出同時有 physical 和 ship component 的 entity 了:<br />
<pre class="notranslate prettyprint lang-rust">let delta = time.delta_seconds();
for (physical, ship, transform) in (&mut physicals, &ships, &transforms).join() {
let acceleration = input.axis_value("accelerate");
let rotate = input.axis_value("rotate");
// handle acceleration -> velocity
let acc = acceleration.unwrap_or_default();
let added = Vector3::y() * delta * acc * ship.acceleration;
let added = transform.rotation() * added;
physical.velocity += Vector2::new(added.x, added.y);
let magnitude = physical.velocity.magnitude();
if magnitude > physical.max_velocity {
physical.velocity *= physical.max_velocity / magnitude;
}
</pre>
最後一步,和上一篇一樣把我們的 system 註冊到 game data 裡面<br />
<pre class="notranslate prettyprint lang-rust">use crate::system::{ShipControlSystem, PhysicalSystem};
let game_data = GameDataBuilder::default()
// Render, transform bundle here ...
.with(ShipControlSystem, "ship_control_system", &["input_system"])
.with(PhysicalSystem, "physical_system", &["ship_control_system"]);
</pre>
註冊 system 的 <a href="https://docs.amethyst.rs/stable/amethyst/prelude/struct.GameDataBuilder.html#method.with" target="_blank">with</a> 是可以寫明相依關係的,三個參數分別是 system struct,system name 和 dependencies list,我們這裡的寫法 input_system(這個名字應是預設的)、ShipControlSystem、PhysicalSystem 會依序執行。<br /><br />
</div>Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-89111479707122650312020-06-29T00:15:00.002+08:002020-07-01T00:07:15.442+08:00使用 Amethyst Engine 實作小行星遊戲 - 3 連接輸入這章有點短,但因為接了鍵盤輸入又要介紹處理輸入的 system 的話,篇幅又太長了,以每章都介紹同樣內容的原則獨立出來;對應為 pong tutorial 的<a href="https://book.amethyst.rs/stable/pong-tutorial/pong-tutorial-03.html" target="_blank">第三章前半部</a>。<br />
<br />
我們上一章已經寫了幾個 entity 跟 component,這章要開始進到 system,用來操作 entity 跟 component 的內容,在每個 frame system 都會叫起來執行一次,不做事或者做點變動。<br />
要接使用者的輸入,我們在 config 下面準備第二個設定檔:config/input.ron。<br />
<div class="hl notranslate">
(<br />
axes: {<br />
"rotate": Emulated( neg: Key(Left), pos: Key(Right) ),<br />
"accelerate": Emulated( neg: Key(Down), pos: Key(Up) ),<br />
},<br />
actions: {<br />
"shoot": [[Key(Space)]]<br />
},<br />
)</div>
<div>
<br /></div>
amethyst 中輸入有兩種形式:axes 和 actions<br />
<div>
<ul style="text-align: left;">
<li>axes 模擬的是兩個不同方向的操作,如果是搖桿的話應該能讀到類比的輸入,鍵盤則是兩個按鍵的互斥的輸入,兩個按鍵只會判定有一個被按下</li>
<li>actions 表示數位的輸入,只有按下跟放開兩種狀態。</li>
</ul>
我們這裡創造兩組 axes 輸入命名為 rotate 跟 accelerate,綁定方向鍵;一個 action 輸入接空白鍵。<br />
可以綁定的對象當然<a href="https://docs.amethyst.rs/stable/amethyst_input/enum.Axis.html" target="_blank">不限於鍵盤</a>,比如說 axes 就能綁定鍵盤、控制器(也許是搖桿)、滑鼠、滑鼠滾輪等輸入;Key 能綁定什麼按鍵則請<a href="https://docs.amethyst.rs/stable/amethyst_input/enum.VirtualKeyCode.html" target="_blank">參考文件</a>。<br />
<br />
要接輸入,我們要對遊戲的 main.rs 作些修改,加上新的 input_bundle:<br />
<pre class="notranslate prettyprint lang-rust">use amethyst::input::{InputBundle, StringBindings},
let input_config_path = config_dir.join("input.ron");
let input_bundle = InputBundle::<StringBindings>::new()
.with_bindings_from_file(input_config_path)?;
let game_data = GameDataBuilder::default()
// Render, transform bundle here ...
.with_bundle(input_bundle)?
</pre>
我們產生一個 InputBundle,並用字串來作為讀取 ron 檔案時的 key:也就是上面我們寫的 "rotate", "accelerate" 等。<br />
<br />
這裡我們準備好寫我們第一個 system 了,這裡我們先把所有的 system 都塞在一個 system.rs 裡面,如果分割得更清楚的話,也可以改用 system 的 module 把各系統分到不同的檔案裡面。<br />
system.rs 的內容如下:<br />
<pre class="notranslate hl">use amethyst::{
core::{Transform},
derive::{SystemDesc},
ecs::{Join, ReadStorage, WriteStorage, System, SystemData, Read},
input::{InputHandler, StringBindings},
};
use crate::component::{Ship};
#[derive(SystemDesc)]
pub struct ShipControlSystem;
impl<'s> System<'s> for ShipControlSystem {
type SystemData = (
WriteStorage<'s, Transform>,
ReadStorage<'s, Ship>,
Read<'s, InputHandler::<StringBindings>>,
);
fn run(&mut self,
(mut transforms,
ships,
input): Self::SystemData) {
for (_ship, _transform) in (&ships, &mut transforms).join() {
let rotate = input.axis_value("rotate");
if let Some(rotate) = rotate {
println!("{}", rotate);
}
}
}
}
</pre>
這個 system 很簡單,去讀 input 的值然後印出來,同樣的 system 也只是一個 struct,我們要實作 System<'s> trait ,裡面帶了 run 這個函式,amethyst 引擎會在每個 frame 呼叫各 system 的 run;實作 system 時也要指定對應的 SystemData,SystemData 的內容對應 world 裡面儲存的 entity、component 或是 resource;以我們這個系統為例,它會去修改 transform component、讀取 ship component、讀取使用者輸入。<br />
在 run 裡,for loop 在下一章會更深入的介紹,這裡我們就是去讀取 input 的 axis_value,指定的 key 是 "rotate",得到對應左右鍵的輸入值,rotate 的值會是 Some(-1), Some(0), Some(1) 其中一個。<br />
<br />
最後一步我們要把系統註冊到 game data,在 main.rs 生成 game_data 的地方,加上新的 System<br />
<pre class="notranslate prettyprint lang-rust">use crate::system::{ShipControlSystem};
let game_data = GameDataBuilder::default()
// Render, transform bundle here ...
.with_bundle(input_bundle)?
.with(ShipControlSystem, "ship_control_system", &["input_system"]);
</pre>
執行遊戲的時候試按左右鍵就能看到輸出值的變化了。</div>
Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-79406485096665555992020-06-26T23:40:00.002+08:002020-06-26T23:40:49.770+08:00使用 Amethyst Engine 實作小行星遊戲 - 2 讀入資源畫東西應該是遊戲最基本的功能,除非你是要做什麼矮人要塞之類的 ASCII 遊戲…這種遊戲大概不太有人想玩了。<br />
Amethyst 使用了一套叫 <a href="https://specs.amethyst.rs/docs/tutorials/" target="_blank">specs</a> 的 ECS Entity-Component-System 框架,當然,也是 Rust 寫的,ECS 概念是:所有的遊戲裡面的物件都是一個 entity(實體),上面可以附上很多的 component(部件,或零件),System(系統)則會去操作這些 component,entity 本身只是帶著 component 走的容器,我們後面實作系統就會更明白這點。<div><br />我們先來個簡單的重構,創一個新的檔案:states.rs 並把在 main.rs 裡面的 state 搬出來:<br />
<pre class="notranslate prettyprint lang-rust">use amethyst::{
assets::{AssetStorage, Loader, Handle},
core::transform::{Transform},
prelude::*,
renderer::{Camera, ImageFormat, SpriteSheet, Texture, SpriteSheetFormat, SpriteRender},
};
pub const ARENA_HEIGHT: f32 = 300.0;
pub const ARENA_WIDTH: f32 = 300.0;
pub struct AsteroidGame;
impl SimpleState for AsteroidGame {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
let world = data.world;
}
}
</pre>
這次我們引入更多的 module,載入資源的 assets、控制位置轉換的 transform、Camera、顯示 Sprite 用的元件;下面定義我們場地的大小,最後是我們已經熟悉的 State Struct。</div><div>
從 StateData 裡面可以拿到遊戲的 world,world 裡面會存有所有遊戲的資料:resource、entity 和 component。<br />
<br />
第一步先來產生我們第一個 entity:一台相機。<br />
<pre class="notranslate prettyprint lang-rust">fn initialize_camera(world: &mut World) {
let mut transform = Transform::default();
transform.set_translation_xyz(ARENA_WIDTH * 0.5, ARENA_HEIGHT * 0.5, 1.0);
world
.create_entity()
.with(transform)
.with(Camera::standard_2d(ARENA_WIDTH, ARENA_HEIGHT))
.build();
}
</pre>
我們產生一個位移定在遊戲場景中間,Z 為 1.0 的相機,之後產生的物件 Z 軸會定在 0.0 上面,Amethyst 的座標是第一象限往右往上愈大,以這個相機的可視範圍來說,左下是 (0.0, 0.0) 右上是 (WIDTH, HEIGHT);產生 entity 只需要呼叫 world.create_entity ,並把需要的 component 用 with 塞進去就可以了。<br />
<br />
第二步來產生寫第一個 component,開一個新的檔案 components.rs 並填入下面的內容:<br />
<pre class="notranslate prettyprint lang-rust">use amethyst::{
ecs::prelude::{Component, DenseVecStorage},
};
pub struct Ship {
pub acceleration: f32,
pub rotate: f32,
pub reload_timer: f32,
pub time_to_reload: f32,
}
impl Ship {
pub fn new() -> Self {
Self {
acceleration: 80f32,
rotate: 180f32,
reload_timer: 0.0f32,
time_to_reload: 0.5f32,
}
}
}
impl Component for Ship {
type Storage = DenseVecStorage<Self>;
}
</pre>
component 沒有什麼特別的,就是單純的一個 struct,在我們實作了 Component 之後,就可以把這個 struct 塞進 entity 裡面,Component 裡面可以什麼都沒有,單純做個標記;也可以像現在這樣,存有一個 Ship 所需要的性質。<br />
實作 Component 的時候,都會需要指定不同的儲存方式,specs 裡面有<a href="https://specs.amethyst.rs/docs/tutorials/05_storages.html#densevecstorage" target="_blank">五種不同的儲存方式</a>可選,針對存取速度、記憶體用量有不同的最佳化,我們還是小遊戲的時候,基本上無腦的用 DenseVecStorage 就行了。<br /><br />
第三步我們要載入 Sprite。<br />
回到我們的 states.rs,加上載入 sprite_sheet 用的函式庫<br />
<pre class="notranslate prettyprint lang-rust">fn load_sprite_sheet(world: &World) -> Handle<SpriteSheet> {
let texture_handle = {
let loader = world.read_resource::<Loader>();
let texture_storage = world.read_resource::<AssetStorage<Texture>>();
loader.load(
"texture/ship.png",
ImageFormat::default(),
(),
&texture_storage,
)
};
let loader = world.read_resource::<Loader>();
let sprite_sheet_store = world.read_resource::<AssetStorage<SpriteSheet>>();
loader.load(
"texture/ship.ron", // Here we load the associated ron file
SpriteSheetFormat(texture_handle),
(),
&sprite_sheet_store,
)
}
</pre>
我們把所有圖形資源都放在 assets/texture 裡面,一套資源是一個 png 檔配上一個 ron 檔,ron 檔描述一系列資源的起始座標跟大小,除了 png 檔 amethyst 也能讀入其他的檔案如 3D 模型等;以這邊的 ship.ron 為例,指定圖片大小為 16x16,內含一個 sprite 從 (0,0) 到 (16,16):<br />
<div class="hl notranslate">
List((<br /> texture_width: 16,<br /> texture_height: 16,<br /> sprites: [<br /> (<br /> x: 0,<br /> y: 0,<br /> width: 16,<br /> height: 16,<br /> ),<br /> ],<br />
))<br />
</div>
先用 amethyst 內提供的 loader 把整個 png 檔讀進來,變成 world 內部的 texture resource(資源),resource 和 component 類似但不會綁定在某個 entity 上面,load 函式會回傳一個 Handle<Texture> 指向 AssetStorage<Texture> 裡 png 檔被讀進的位置,Handle 的實作類似 reference count pointer,讓所有人都可以共用一個 asset。<br />
png 檔被讀入 AssetStorage 後,再次使用 loader 把 texture 讀入變成 SpriteSheet,這次回傳的內容會是 Handle<SpriteSheet> 指向 AssetStorage<SpriteSheet> 中的位置。<br />
<br />
最後一步,我們把上面一切都整合起來:<br />
<pre class="notranslate prettyprint lang-rust">fn initialize_ship(world: &mut World, sprite_handle: Handle<SpriteSheet>) {
let mut transform = Transform::default();
transform.set_translation_xyz(ARENA_WIDTH * 0.5, ARENA_HEIGHT * 0.5, 0.0);
let sprite_render = SpriteRender {
sprite_sheet: sprite_handle,
sprite_number: 0
};
world
.create_entity()
.with(transform)
.with(sprite_render.clone())
.with(Ship::new())
.build();
}
</pre>
initialize_ship 跟 initialize_camera 沒有差太多,參數的 sprite_handle 來自剛剛的函式 load_sprite_sheet;位移設定太空船的位置在畫面正中間,從 sprite_sheet 產生 sprite_render,SpriteRender 就是真的顯示在畫面上的物件了,如果一組 sprite 裡面有數個 sprite,可以用 sprite_number 去指定要顯示哪個;最後生成 entity 並把 transform、sprite、Ship 三個 component 加上去即可。</div><div>有關 Sprite 的部分,因為有點複雜,我簡單整理起來是這個樣子:</div><div><ol style="text-align: left;"><li>呼叫 Loader 將圖片載入為 Texture -> Handle<Texture></li><li>呼叫 Loader 讀 ron 檔,將圖片分割為 SpriteSheet -> Handle<SpriteSheet></li><li>從 Handle<SpriteSheet> 生成 SpriteRender -> 作為 Component 放到 entity 中</li></ol></div><div>
<pre class="notranslate prettyprint lang-rust">impl SimpleState for Asteroid {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
let world = data.world;
let sprite_sheet = load_sprite_sheet(world);
world.register::<Ship>();
initialize_camera(world);
initialize_ship(world, sprite_sheet);
}
}
</pre>
在 state on_start 時,呼叫 load_sprite_sheet 得到 Handle<SpriteSheet>,呼叫 initialize_camera 和 initialize_ship 初始化 camera 跟 ship entity。<br />
有一行特別是這行 world.register::<Ship>()<br />
如果不加這行的話,會遇到執行期錯誤:<br />
<div class="hl notranslate">
thread 'main' panicked at 'Tried to fetch resource of type `MaskedStorage<Ship>`[^1] from the `World`, but the resource does not exist.<br />
<br />
You may ensure the resource exists through one of the following methods:<br />
<br />
* Inserting it when the world is created: `world.insert(..)`.<br />
* If the resource implements `Default`, include it in a system's `SystemData`, and ensure the system is registered in the dispatcher.<br />
* If the resource does not implement `Default`, insert it in the world during `System::setup`.<br />
<br />
[^1]: Full type name: `specs::storage::MaskedStorage<rocket::entities::Ship>`', /home/yodalee/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/macros.rs:16:9<br />
</div>
<br />
這個原因是 component 內的 storage 要經過初始化才能使用,我們在 entity 裡面使用了 Ship 這個 component 卻沒初始化 storage,程式就爆掉了,register 就是向 world 註冊並初始化 component;未來我們加上 System 之後,只要有被 System 使用的 Component 都會自動初始化,這行就不需要了。<br />
經過這麼一團千辛萬苦,現在執行起來就能看到飛船在中間的畫面了。<br />
<div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0-NoVK2FIIlYESMBBpkqlCcO2g7cYlIQ1Ce_Kd3uCaxWT02eFPZuhq305FACHpK_16zcK7GYnMBKFtgzzIR2S3FBrfMNZh6ImAl9xEnpyREaQT2a1XGSxseThJi2M4LxO7VhusuWj9fw_/s1009/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2020-06-26+17%253A26%253A40.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1009" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0-NoVK2FIIlYESMBBpkqlCcO2g7cYlIQ1Ce_Kd3uCaxWT02eFPZuhq305FACHpK_16zcK7GYnMBKFtgzzIR2S3FBrfMNZh6ImAl9xEnpyREaQT2a1XGSxseThJi2M4LxO7VhusuWj9fw_/s320/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2020-06-26+17%253A26%253A40.png" width="320" /></a></div><div><br /></div></div>Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-38436644479661172842020-06-26T00:35:00.005+08:002020-08-10T23:30:04.656+08:00使用 Amethyst Engine 實作小行星遊戲 - 1 設定專案首先我們要先設定專案,內容會對應官方教學的 <a href="https://book.amethyst.rs/stable/getting-started.html" target="_blank">Getting Started</a> 和 Pong 的<a href="https://book.amethyst.rs/stable/pong-tutorial/pong-tutorial-01.html" target="_blank">第一章</a>:<br /><br />設定專案上我個人建議是使用 amethyst 自己的工具,可以用 Cargo 安裝比較簡單;不然就要 clone 官方提供的啟動專案,也不是不行但就不能帶著走了;再其次當然就是用 Cargo new 生一個空專案,在 Cargo.toml 裡面加上 amethyst 的 dependency,這個真的很麻煩完全不建議:
<div class="hl notranslate">cargo install amethyst_tools<br />amethyst new <game-name><br /></div><br />專案會自動新增 Cargo.toml,比較重要的是下面的 features,amethyst 可以選擇依賴的底層 API,如果是 Linux/Windows 選 Vulkan;蘋果用戶選 Metal;empty 我編譯完成圖形介面會出不來所以就不要用了。<br />
<pre class="hl notranslate">[package]<br />name = "rocket"<br />version = "0.1.0"<br />authors = []<br />edition = "2018"<br />[dependencies]<br />amethyst = "0.15.0"<br />[features]<br />default = ["vulkan"]<br />empty = ["amethyst/empty"]<br />metal = ["amethyst/metal"]<br />vulkan = ["amethyst/vulkan"]</pre>
<div>專案建好可以直接先開始編譯,amethyst 框架滿大的,第一次編譯會花上幾分鐘的時間把整個框架給架起來,不過放心,編好一次之後編譯都只要編譯你寫的 code,速度會快很多(雖然說我覺得還是很慢,大概 30 秒至一分鐘不等,Rust 真的很適合<a href="https://xkcd.com/303/" target="_blank">這張圖</a>)。<br /><div class="separator" style="clear: both; text-align: left;"><a href="https://imgs.xkcd.com/comics/compiling.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="360" data-original-width="413" src="https://imgs.xkcd.com/comics/compiling.png" /></a></div><a href="https://blogger.googleusercontent.com/img/proxy/AVvXsEjGtZ5TAQ-D3CKcpHZo6Gi7ehx6hxmGxNuqCTlZEzQAY_GoHBNmkmUUa3EB_OAUT-OYsl2ipJ4vDfEq5md_09zmEHilXQm3OYCqMrlQEIsmvy7EZ8Dw2-weooyUSOEPHABH1g8KUjnZaArl=s413" style="margin-left: 1em; margin-right: 1em; text-align: center;"></a><br />預設版本執行起來應該會看到完全空白的畫面,生成的 main.rs 如下所示:<br />
<pre class="notranslate prettyprint lang-rust">use amethyst::{<br /> core::transform::TransformBundle,<br /> prelude::*,<br /> renderer::{<br /> plugins::{RenderFlat2D, RenderToWindow},<br /> types::DefaultBackend,<br /> RenderingBundle,<br /> },<br /> utils::application_root_dir,<br />};</pre>
一開始當然是引入需要的模組。<br />
<pre class="notranslate prettyprint lang-rust">struct MyState;<br />impl SimpleState for MyState {<br /> fn on_start(&mut self, _data: StateData<'_, GameData<'_, '_>>) {}<br />}</pre>
定義遊戲的 struct,amethyst 把遊戲分為不同的狀態,例如遊戲開始的時候會載入資源,這時候要顯示載入中;載入完要顯示選單,在 amethyst 裡面這些都是不同的狀態,遊戲本質上來說就是在這些狀態間切來切去。Amethyst 使用一個 stack 來管理 state,最上層就是目前執行的 state。<br />不過呢,因為現在要做的範例還沒有這麼複雜,state 的部分我們就先跳過,我自己也還沒學會(欸,這應該要等到最後我們開始把遊戲外面包上介面的時候再來學就好了。<br />這邊使用 amethyst 提供的 SimpleState trait,它已經幫我們實作了事件的介面,比自己實作完整 state 簡單。<br />
<pre class="notranslate prettyprint lang-rust">fn main() -> amethyst::Result<()> {<br /> amethyst::start_logger(Default::default());<br /> let app_root = application_root_dir()?;<br /> let assets_dir = app_root.join("assets");<br /> let config_dir = app_root.join("config");<br /> let display_config_path = config_dir.join("display.ron");</pre>
進到主程式第一件事就是先打開 logger(然後我找不到怎麼記下 log 的文件…),用來記錄事件/警告/錯誤等等。<br />設定 assets_dir 跟 config_dir 並讀入 display.ron 設定檔,<a href="https://github.com/ron-rs/ron" target="_blank">ron</a> 是款 rust 專門的格式, config/display.ron 會記錄視窗的標題,還有它的畫面尺寸等資訊:<br />
<pre class="notranslate">(<br /> title: "rocket",<br /> dimensions: Some((1000, 1000)),<br />)</pre>
所有可以設定的選項可以參考 DisplayConfig 的<a href="https://docs-src.amethyst.rs/stable/amethyst_window/struct.DisplayConfig.html" target="_blank">文件</a><br /><pre class="notranslate prettyprint lang-rust"> let game_data = GameDataBuilder::default()<br /> .with_bundle(<br /> RenderingBundle::<DefaultBackend>::new()<br /> .with_plugin(<div> RenderToWindow::from_config_path(display_config_path)?<br /> .with_clear([0.34, 0.36, 0.52, 1.0]),<br /> )<br /> .with_plugin(RenderFlat2D::default()),<br /> )?</div><div> .with_bundle(TransformBundle::new())?;<br /><br /></div><div> let mut game = Application::new(assets_dir, MyState, game_data)?;<br /> game.run();<br /> Ok(())<br />}<br /></div></pre>剩下的 main 內容就是把 game_data 建起來,預設我們引入兩個 bundle,用來顯示的 RenderingBundle 跟做圖形轉換用的 TransformBundle。<br />首先我們生一個 RenderingBundle,裡面有兩個 plugin,RenderToWindow 讀入我們寫好的 display.ron 並顯示主視窗;RenderFlat2D 可以在畫面上顯示 Sprite。<br />最後拿著我們生成的遊戲狀態 MyState、 game_data 全部塞進 Application 裡面就可以了;看到這麼一大團程式碼,就可以理解為什麼一定要用 amethyst 工具幫我們生出預設的設定了,自己手爆這堆東西一定會累死。<br /><br />到這邊我們就寫好一個空白視窗的小程式了,下一步我們要在畫面上畫上一些東西。<br /><div><div class="separator" style="clear: both; text-align: center;"><br /></div></div></div><div class="separator" style="clear: both; text-align: center;"><br /></div>Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-49314451989933612582020-06-24T23:51:00.002+08:002020-06-26T00:36:24.366+08:00使用 Amethyst Engine 實作小行星遊戲 - 目錄<div class="separator">
故事是這個樣子的,之前因為武肺的關係耍廢了一陣子,你看 blog 都沒更新幾個月了,只有中間在那邊玩 vscode 整個就是魯廢。</div>
最近受到強者我同學在歐陸大殺四方的呂行大神感召,試玩了一下 Rust 的 amethyst (紫水晶,託名字的福一定要查 rust amethyst 才會查到要的東西)框架,決定來寫點文介紹一下。<br />
<br />
當初看到 amethyst 是在 rust 的 <a href="https://arewegameyet.rs/" target="_blank">are we game yet 頁面</a>上看到的,如果你只是要寫簡單遊戲的話,rust 有另一套也算知名的引擎 <a href="https://github.com/PistonDevelopers/piston" target="_blank">Piston</a>,用量目前比 amethyst 還要高一截,但我覺得 piston 的潛力不及 amethyst,雖然完整但 piston 在虛擬化上面沒有 amethyst 這麼高階,導致很多東西還是要設計師自己跳下去設計,相對來說就是學習曲線比較淺,有經驗的話看看文件就能上手。<br />
不過,現下一般來說找不太到用 piston 或 amethyst 寫的大型遊戲,在範例頁面兩者做的都只是些老遊戲;不過話說回來做遊戲本來跟遊戲引擎就沒什麼關係,比較像是你整體企劃跟資源有沒有弄好,沒引擎還是可以寫個爆紅的 2048 或 flappy bird,有了好引擎還是會搞出歷史性的糞作,像是最後生(消音。<br />
<br />
總而言之 amethyst 是(另)一套遊戲框架,背後的設計邏輯是所謂的 ECS:entity、component、system,是有人說 ECS 在 gaming 有 buzzword 的意味,但畢竟兩個比較大的遊戲引擎:unity 跟 unreal 都用上了 ECS 的概念,我想這部分應該是沒什麼疑慮。<br />
在這個系列文,我預計會用 amethyst 寫一個打小行星的經典小遊戲,先聲明這的專案是從這個<a href="https://github.com/udoprog/asteroids-amethyst" target="_blank">同樣的專案</a>複製而來,它也是用 amethyst 寫的,只是年代久遠現在已經編不起來了,我直接拿了它的素材來用(應該是不至於被吉吧Orz),完成的畫面應該會如下所示:<br />
<div style="text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWvzPOcQD5ogHhn0ExhsiCHfEl3Guc3hwYWw1ulndqOw3UMlifYLmL0jRk6zRfIZFDHQyCjH9IakeC-voq1fJo8Z8LnR0683MnPmh1TiVT92kRv5v9-1hQUAfbp2qOfDMKW2aljsi7aDGy/s1009/test.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1009" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWvzPOcQD5ogHhn0ExhsiCHfEl3Guc3hwYWw1ulndqOw3UMlifYLmL0jRk6zRfIZFDHQyCjH9IakeC-voq1fJo8Z8LnR0683MnPmh1TiVT92kRv5v9-1hQUAfbp2qOfDMKW2aljsi7aDGy/s320/test.png" width="320" /></a></div>
<br />
<br />
如果去看 amethyst 的教學文,它有用 amethyst 寫一個 <a href="https://book.amethyst.rs/stable/pong-tutorial.html" target="_blank">pong 遊戲</a>,但我覺得 pong 不算一個好的例子,它不會生成跟刪除新的物體,偏偏這應該是很多遊戲必備的功能,用打小行星這種比較能示範怎麼做。<br />
總之讓我們開始吧,這篇就作一個目錄的角色,用來連接所有教學文,希望能對大家成功傳教(欸。Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-42535728443347602582020-05-16T00:58:00.003+08:002020-05-16T17:18:17.862+08:00第一次跳槽 vscode 就上手故事是這樣子的,小弟第一次學寫 code 的時候,是在大一修計算機程式(嚴格來說是高三下學期上了幾個小時的 C,不過那實在稱不上是"學")的時候,第一個使用編輯器是破舊破舊的 Dev C++ ,我打這篇的時候差點都忘了它叫 Dev C++ 了。<br />
當然那時候的功力跟現在實在是天差地遠,淨寫一些垃圾,啊雖然現在也是淨寫一堆垃圾…。<br />
總之後來應該是大二,被同學們拉去演算法課上當砲灰,第一次接觸了工作站 + vim,從那時候把 Dev C++ 給丟了跳槽到 vim,就一直用到現在,之中當然也會用一下其他的編輯器,像是改 windows 的 .NET 程式用到 Visual Studio,但大體還是以 vim 為主力,算算也是超過 10 年的 vimer 了。<br />
<br />
不過這兩三年在工作上、日常 project 上面,多多少少都見識到 vim 的不足之處,例如新語言(主要是 Rust)支援不足、跟編譯除錯工具整合不佳、跟 GUI 整合不佳、跟 Git 整合不佳要另外開終端機跟 gitg、自動格式化/排版操作麻煩而且通常排不好;正好此時 Microsoft 回心轉意擁抱開源,推出了 vscode,隔壁棚的 emacs 有大神跳槽<a href=" https://gist.github.com/kuanyui/11be51ee7894a9f01ce438a97dcffcb6" target="_blank">鬧得風風雨雨</a>,台灣 CUDA 第一把交椅強者我同學 JJL 也跳槽 vscode 惹還來傳教。<br /><br />
正好最近寫 code 沒什麼靈感,而且最近正好武漢肺炎的關係時機歹歹,就來試著跳槽一下吧(?,到目前為止用 vscode 對最近碰的一個 ncollide package 做了一些除錯的工作,筆記一下到目前為止的設定還有使用方式的筆記。<br />
<br />
vscode 基本上的優勢就是是它編輯/建構/除錯三位一體的編輯介面;還有它的擴充功能,用過的都說讚。<br />
擴充方面主要參考的文件有兩個:<a href="https://larrylu.blog/vscode-tips-fe3320f9032a" target="_blank">VSCode 如何提高我的寫扣效率</a>、 <a href="https://blog.goodjack.tw/2018/03/visual-studio-code-extensions.html" target="_blank">小克的 Visual Studio Code 必裝擴充套件</a>,另外台灣 CUDA 第一把交椅強者我同學 JJL 大大也有推薦一些:<br /><br />
擴充的安裝方式是按快捷鍵 Ctrl + P,打入 ext install 後面接套件名,下面擴充的連結裡面也有顯示安裝的指令:<br />
<br />
<a href=" https://marketplace.visualstudio.com/items?itemName=vscodevim.vim" target="_blank">vim 擴充</a>,讓 vscode 的編輯介面套用 vim 的操作方式,想要手跟鍵盤黏踢踢就一定要裝<br />
語言相關:<br />
<a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools" target="_blank">C/C++ 擴充</a>:還沒試用只是覺得起家的 C++ 必須裝一下:<br />
<a href=" https://marketplace.visualstudio.com/items?itemName=ms-python.python" target="_blank">Python 擴充</a>:一樣還沒試用只是覺得有一天會寫到先裝一下:<br />
<a href="https://marketplace.visualstudio.com/items?itemName=rust-lang.rust" target="_blank">Rust 擴充</a>:這個是這次語言唯一試用過的,雖然結果不怎麼樣<br /><a href="https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb" target="_blank">codelldb 除錯擴充</a>,可以用 LLVM 的 lldb 對程式除錯,裝了這個是為了要對 Rust 除錯 <br /><br /><div>
工具類:</div>
<a href=" https://marketplace.visualstudio.com/items?itemName=mhutchie.git-graph" target="_blank">Git Graph</a>:整合 gitg 類似的圖形化顯示工具到介面,git 管理上當然可以靠打字,但看歷史還是看圖方便<br />
<a href=" https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens" target="_blank">GitLens</a>:還沒試過,強者我同學 JJL 推薦的<br />
<a href=" https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.todo-tree" target="_blank">TODO tree</a>:統一管理 project 內部的 TODO, FIXME, XXX<br />
<a href=" https://marketplace.visualstudio.com/items?itemName=shardulm94.trailing-spaces" target="_blank">Trailing Spaces</a>:自動刪掉程式碼行尾的空白<br />
<a href=" https://marketplace.visualstudio.com/items?itemName=bierner.markdown-preview-github-styles" target="_blank">Markdown Github Style</a>:編輯 markdown 文件時可以直接預覽輸出的格式,解決每次編輯 Github README.md 都要一直 push -f 直到格式完全改對為止,這點很強烈的突顯出 vim 等純文字編輯器的弱項,無法和圖形整合,以致在 markdown、LaTex 這類文字和顯示有相互關係的文件編輯會很吃虧(好啦好啦我知道有人能人腦 render latex 的)。<br /><br />
怎麼建構專案?<br />
在 vscode 裡面的建構叫 <a href=" https://code.visualstudio.com/docs/editor/tasks" target="_blank">task</a>,在選單 terminal 下面的 run Task 跟 run Build Task (Ctrl + Shift + B),沒有 cargo 預設的話就要自行編輯 tasks.json,以下是我這次 debug 時使用的 tasks.json<br />
<pre class="notranslate prettyprint lang-json">{
"version": "2.0.0",
"tasks": [
{
"label": "cargo run",
"type": "shell",
"command": "cargo",
"args": ["build"],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
</pre>
應該滿直覺的,就是呼叫 cargo build 幫我編譯整個專案;在寫完 code 之後使用快捷鍵 Ctrl + Shift + B 就能編譯專案了。<br />
<div><br /></div><div>如何除錯:<br /></div>
<a href="https://code.visualstudio.com/docs/editor/debugging" target="_blank">除錯</a>是 vscode 一項殺手級的功能,vscode 公開一個 API 讓安裝的語言擴充使用,需要什麼語言的除錯安裝擴充就好,像我上面就安裝了 C/C++, Python, Rust 的擴充。如果我記憶沒錯的話,跟 visual studio 一樣,vscode 快捷鍵是也執行 Ctrl + F5 跟除錯 F5:<br /><br /><div>
用 Ctrl + Shift + D 展開 debug 介面。</div><div>理論上用滑鼠在原始碼旁邊點一下就能加上 breakpoint 不知道是 Rust 還是 lldb 的問題,我用滑鼠加上去的 breakpoint 都煞不住,至少一定要先在 debug console 裡下一個 b main 讓程式煞住之後,用滑鼠加的 breakpoint 才會有用,真的很奇怪。</div>
另外就是 debug console 的指令跟習慣的 gdb 有點不同要重新習慣,最奇怪的大概是按了 enter 竟然不會重複上個指令,這樣要一直按 n + enter + n + enter 怪麻煩的,只能去習慣 vscode 的除錯指令:F10/next、F11/step、Shift+F11/finish 了。<br />
<br />
這次最主要的目的是要對 Rust 程式除錯,我參考的是下面<a href="https://www.forrestthewoods.com/blog/how-to-debug-rust-with-visual-studio-code/" target="_blank">這篇文章</a>…不過試用之後沒有成功<br />
首先我們要加上一個 launch.json 告訴 vscode 要怎麼跑除錯的程式:<br />
<pre class="notranslate prettyprint lang-json">{
"version": "0.2.0",
"configurations": [
{
"name": "Debug example contact_query2d",
"type": "lldb",
"request": "launch",
"program": "${workspaceRoot}/target/debug/examples/contact_query2d",
"args": [],
"cwd": "${workspaceRoot}",
}]
}
</pre>
再來用 F5 就能開始除錯了,但不知道為什麼我 step into 一個函式,瞬間都變成 assembly code,連 stack 資訊都爛掉了,根本無從 debug 起,感覺是 vscode 哪裡跟 lldb 沒弄好,不過我覺得這不是 vscode 的問題,畢竟我在終端機用 rust-gdb 一樣會有問題,正好反過來如果下 b main 停下來的話,rust-gdb 下一步會停不下來,一口氣跑到 main 的尾巴…。<br />
這個問題一時之間好像無解,也許要等 rust 跟 codelldb/gdb 真的接好之後再來看看了。<br />
<br />
下面就是個零碎操作:<br /><div>
Ctrl + KT 叫出 color theme 設定,我用的是 light 的 Solarized Light ,最近眼睛好像不太適合全黑的畫面了QQ。</div><div><br /></div>
回頭一看怎麼一堆快捷鍵,不過算啦,跟 vim 的快捷鍵比起來這還是算少的吧XD;話說大概是「把手留在核心區」這個哲學的關係,vim 大部分的按鍵都少有用 Ctrl/Alt 開頭的,剛好一般的圖形應用程式包括 vscode ,大部分的快捷鍵都是 Ctrl/Alt 開頭,也因此在操作上面,vim 很容易就能跟桌面應用程式整在一起,就像是瀏覽器的 vimium 跟 vscode vim 擴充。<br />
<br />
Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com2tag:blogger.com,1999:blog-2090850666133584855.post-11640321627707416532020-04-11T15:06:00.000+08:002020-04-11T15:06:09.315+08:00用 docker container 來編譯程式故事是這樣子的,最近受朋友之託研究一個套件,在編譯的時候…不知道為什麼我的 Archlinux 編不起來,有某個奇怪的 bug <strike>蒙蔽了我的雙眼</strike>擋住了我而且一時之間解不掉,目前看起來像是 golang 那邊的鍋。<br />
總之目前看起來像是 Archlinux 限定的問題,如果裝一台 ubuntu 18.04 的虛擬機,在裡面 build 就沒這個問題,可以完成編譯。
不過想想,現在都 2020 年了,怎麼連 docker 怎麼用都還沒學起來(查一下這玩意 2013 年就已經問世了耶…),就本例來說沒事還要開一個巨大的 virtualbox ,建個巨大的虛擬磁碟再安裝作業系統真的有點划不來,就花了點時間學了一下 docker ,然後順便記個筆記,不然這年頭發現自己學習能力低落,連 docker 這麼簡單的東西都學不好QQ。<br />
<br />
其實網路上已經有 100 篇 docker 的教學文了,不過我還是來寫個 101 篇吧。<br />
首先是關於 docker,大抵上就是一個輕量化的容器,在主機作業系統之上為每個應用程式建立一個最小的執行環境,每個 container 都是一個 user space process;相對的虛擬機則是把整個作業系統都包進去,每個虛擬機共用一個硬體,這部分就偷用一下他們官網的圖片。<br />
<br />
<table><tbody>
<tr>
<td><a href="https://www.docker.com/sites/default/files/d8/2018-11/container-vm-whatcontainer_2.png" imageanchor="1"><img border="0" data-original-height="639" data-original-width="800" height="192" src="https://www.docker.com/sites/default/files/d8/2018-11/container-vm-whatcontainer_2.png" width="240" /></a></td>
<td><a href="https://www.docker.com/sites/default/files/d8/2018-11/docker-containerized-appliction-blue-border_2.png" imageanchor="1"><img border="0" data-original-height="639" data-original-width="800" height="192" src="https://www.docker.com/sites/default/files/d8/2018-11/docker-containerized-appliction-blue-border_2.png" width="240" /></a></td>
</tr>
</tbody></table>
<br />
當然我們這裡只是要用,背後的原理要是我哪天學會的話再來寫文章記錄QQ,對比虛擬機 docker 具有小、快的優點,畢竟不用開一台機器就要裝一次作業系統,很適合像我這樣只是要用另一個作業系統做個測試,或者寫網路服務的,可以讓程式跑在一個固定的環境裡面,不用一台一台虛擬機處理環境的問題。<br />
<br />
這次我的目標就是開一個 ubuntu 18.04 的作業系統,然後在裡面進行編譯。
以我的 archlinux 來說,第零步是先安裝並啟動 docker:<br />
<div class="hl notranslate">
pacman -S docker<br />
systemctl start docker.service
</div>
<br />
為求方便的話可以把自己加入 docker group 裡面,不過這等同於給他 root 權限(這段警語只出現在英文 wiki 上面):<br />
<div class="hl notranslate">
gpasswd -a user docker
</div>
<br />
第一步當然就是先把 ubuntu 18.04 的 image 給拉下來,不加版號的話會拉下最新的版本,這裡的 ubuntu image 是 ubuntu 官方準備好,並且放到 <a href="https://hub.docker.com/" target="_blank">docker hub</a> 上面供大家下載的版本,是一套非常純粹的 ubuntu,映像檔最小化連 python 都沒有;大家可以視自己的需求選擇其他的版本,像是 node 官方也有出自己含 node.js 的映像檔,python、django、mysql …都有對應的映像檔可以選擇。<br />
如果自己註冊 docker hub 的帳號,也可以把自己建構的映像檔上傳到 docker hub 上讓大家下載,不過我這篇不會介紹,有興趣的請自己參考這兩篇:<a href="https://www.blogger.com/"><span id="goog_1712871779"></span>一<span id="goog_1712871780"></span></a>、<a href="https://medium.com/larry-blog/share-image-on-dockerhub-ccb7d9b26fa8" target="_blank">二</a>。 <br />
<div class="hl notranslate">
docker pull ubuntu:18.04
docker pull ubuntu
</div>
<br />
載好之後就可以在 docker image ls 或 docker images 看到 ubuntu 了:
<br />
<div class="hl notranslate">
$ docker image ls<br />
REPOSITORY TAG IMAGE ID CREATED SIZE<br />
ubuntu 18.04 4e5021d210f6 3 weeks ago 64.2MB<br />
ubuntu latest 4e5021d210f6 3 weeks ago 64.2MB</div>
<br />
有了 image 就可以把 container 給跑起來,可以想像 image 就是把需要的檔案都拿到手裡,把 image 放到 container 裡面跑起來就會變得像一個真的作業系統一樣。<br />
docker run 可能是 docker 最複雜的指令之一,選項多到不可理喻,我們先從簡單的開始:
<br />
<div class="hl notranslate">
docker run -it ubuntu:18.04 bash
</div>
<br />
執行一個 ubuntu 18.04 的容器,-it 讓 docker 打開虛擬終端機,並執行 bash,這時候我們就會進到 ubuntu 的 bash,可以從 lsb-release 裡面看到這真的是一台 ubuntu 的機器。
<br />
<div class="hl notranslate">
root@e98deb8ccdaf:/# ls<br />
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var<br />
root@e98deb8ccdaf:/# cat /etc/lsb-release<br />
DISTRIB_ID=Ubuntu<br />
DISTRIB_RELEASE=18.04<br />
DISTRIB_CODENAME=bionic<br />
DISTRIB_DESCRIPTION="Ubuntu 18.04.4 LTS"
</div>
<br />
開另一個 host 的終端機,用 docker container ls 或是 docker ps 也能看到它在運作:
<br />
<div class="hl notranslate">
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES<br />
e98deb8ccdaf ubuntu:18.04 "bash" 59 minutes ago Up 59 minutes inspiring_feistel
</div>
<br />
但這個 container 在我們下 exit 離開的時候,它也會跟著不見,要用 docker container ls -a 把執行中跟已經被關掉的 container 都列出來才會看到它。<br />
這多少顯示了 docker 隨開隨用,不用隨關的特性,下個 run 就開了一個,不用了它就被關掉了。
於是我們可以在 run 的時候,改成這樣下:
<br />
<div class="hl notranslate">
docker run -itd --name blogger ubuntu:18.04
</div>
<br />
首先是 -d 這個參數,會讓 docker 在背景把這個機器給開起來;--name 則是給機器一個別名,這樣就不需要去動到前面 docker container ls 裡面的 CONTAINER ID,畢竟打名字還是比打 hash 的 hex value 簡單多了。<br />
下完這行 docker 會給出新產生機器的 hash value,docker ps 也可以看到:
<br />
<div class="hl notranslate">
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES<br />
35f40d006a5f ubuntu:18.04 "/bin/bash" 1 second ago Up Less than a second blogger</div>
<br />
這時候我們可以用 exec 進到這台 container,這樣跟 run -it 的效果是一樣的,只是這次離開 container 之後它還是會繼續執行,blogger 的位置換成它的 container ID 35f4 也可以,以下同:
<br />
<div class="hl notranslate">
docker exec -it blogger bash
</div>
<br />
把它停掉可以用 stop 正常關掉這個 container 或是 kill 直接砍了它:
<br />
<div class="hl notranslate">
docker stop blogger
docker kill blogger
</div>
<br />
就算是關掉的 container 在 docker ps -a 還是看得到它,可以用 restart 把它開回來
<br />
<div class="hl notranslate">
docker restart blogger
</div>
<br />
為了要用 ubuntu 的機器編譯,我們還要將外部的檔案放到 container 內部,docker 對應的機制可以用 docker cp,有點像是 scp 的下法:
<br />
<div class="hl notranslate">
docker cp <file> <container>:<file>
docker cp ~/server.py blog:/server.py
docker cp blog:/server.py ~/server.py
</file></container></file></div>
<br />
或者我們想要簡單一點,可以用 volume 的方式,這有點像是 virtualbox 裡面的共享資料夾,平時 container 跟 host 之間可以用這個資料夾互通有無,且就算 container 被刪掉了,這個資料夾還是會留著;詳細的 volume 介紹可以看<a href="https://larrylu.blog/using-volumn-to-persist-data-in-container-a3640cc92ce4" target="_blank">這篇</a>,我這裡是直接用它的第二種方式,直接在 run 的時候指定一個資料夾給 container:<br />
<div class="hl notranslate">
docker run -v ~/docker:/docker -it ubuntu:18.04 bash
</div>
<br />
就能在內部的 /docker 裡面看到 host 那邊 ~/docker 的檔案了:
<br />
<div class="hl notranslate">
root@c518b1b9fd7b:/# ls docker/<br />
Dockerfile
</div>
<br />
如果要用 docker 當個編譯工具的話,差不多是這樣就夠了,連續的指令打起來就是:
<br />
<div class="hl notranslate">
docker run -v ~/docker:/docker -itd --name compile ubuntu:18.04<br />
docker exec -it compile bash<br />
root@c518b1b9fd7b:/# apt update
...
</div>
<br />
全新的 18.04 ubuntu 真的是超級單純,該裝的東西都要自己裝好,連 apt 都要自己 update,但我編譯下去它還真的編譯過了 WTF……到底 archlinux 是出了什麼問題…。<br />
<br />
本篇文章感謝強者我同學在新加坡大殺四方稱霸麻六甲海峽的志賢大大多加指導。
Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-58556820773687961962020-03-14T23:38:00.000+08:002020-07-13T01:12:17.231+08:00幫 Google Assistant 加上更多語言<img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhN7KQsx41-T0KGY0VfU7rBKsQ8noul9QpNXB2kdzLODWOb6xN0i6KJ57G6WEOLCFEoipfGRphqvbAoUTvoogOJb95-wyOHU81yh676G1v3l_bYzldjcDnoCXlb3vYRMzPTf_Mi-XGvCUR/s320/test.png" style="display: none;" /><a href="https://yodalee.blogspot.com/2020/01/google-assistant-guess-number.html" target="_blank">上一篇</a>我們成功做出一個會跟我們猜數字的 Assistant,不過因為方便我開發的時候都是用英文在開發,自己玩玩當然 OK,但對非英文使用者就不行了,因此我們來試著加上中文的回應。<br />
目前 google assistant 支援 20 種語言(註),在 assistant 的開發頁面選擇 modify language setting 可以打開想要的語言,我這裡先開英文和繁體中文,兩個語言都要設定 assistant 的呼叫詞,中文的我們就叫它「猜數字程式」。<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEix5Bzzz8stqMcgDmE3Felm1_tk7A-ml07LOW-cPimcyh5lJ42AY_Wh6ri1YNMJ7KqglL3pGAHaZz5OalPhu4sl_dgPH_c_VxO7bPEu9eizGgpcoyROfxR1BIEEqwC1UO1ZKEKZ5Vh_PgCY/s1600/languagesetting.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEix5Bzzz8stqMcgDmE3Felm1_tk7A-ml07LOW-cPimcyh5lJ42AY_Wh6ri1YNMJ7KqglL3pGAHaZz5OalPhu4sl_dgPH_c_VxO7bPEu9eizGgpcoyROfxR1BIEEqwC1UO1ZKEKZ5Vh_PgCY/s320/languagesetting.png" width="320" /></a></div>
<br />
在連接的 dialogflow 的部分,點選 project 旁邊的小齒輪,language 分頁裡面也加上繁體中文的選項,在左側欄的 project 名稱下面就可以看到有兩個不同的語言。<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwzagTefAz_ib5aMOlVdtFgqq4_CseHxtNd9df1KmGacXAejRd2wDQMM-D50W8KHf9ThC8F5OoodocPsm-_J-RW43RsD28Ss8j2cX9wRaoeYZpZTXGhUW-5lSFO7bU2XMBRVIn44BXtAw-/s1600/dialogsetting.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwzagTefAz_ib5aMOlVdtFgqq4_CseHxtNd9df1KmGacXAejRd2wDQMM-D50W8KHf9ThC8F5OoodocPsm-_J-RW43RsD28Ss8j2cX9wRaoeYZpZTXGhUW-5lSFO7bU2XMBRVIn44BXtAw-/s320/dialogsetting.png" width="320" /></a></div>
<br />
加完語言之後,最麻煩的就是把整個設定複製一份了,所有的 intent 設定在新的語言都要再設定一遍,或者你在不同的語言想要採取不同的 flow 也可以,因為猜數字很簡單,我英文跟中文就用相同設定,training phrase 的部分,直接打上中文就可以了。<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjvu0BPCiJRp5NkovJEE7jYpcIB50OUOnZJYp8O1Y-VB-wtPJZpir06a9qgWOjfDOGJ-zYUAnDS54PhQAR6m8-J7K7vw0qdoNc2jaqii1gW-5RcT5P72-KvvYzLr1HSgP5hLA5s99OioJFW/s1600/training.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjvu0BPCiJRp5NkovJEE7jYpcIB50OUOnZJYp8O1Y-VB-wtPJZpir06a9qgWOjfDOGJ-zYUAnDS54PhQAR6m8-J7K7vw0qdoNc2jaqii1gW-5RcT5P72-KvvYzLr1HSgP5hLA5s99OioJFW/s320/training.png" width="320" /></a></div>
<br />
<br />
設定完 dialogflow 之後,下一步要設定我們的 webhook 讓它也能處理多國語言,我選用的套件是 python 的 <a href="https://github.com/danhper/python-i18n" target="_blank">python-i18n</a>,可以用 yml 或 json 格式儲存想要翻譯的文字,這樣我們程式碼幾乎結構不用大修,只要在回應的地方呼叫 i18n 幫我們吐出翻譯過的文字就好。<br />
修改後的目錄長成這樣:<br />
<pre class="notranslate prettyprint">main.py
locales
| guess.en.json
| guess.zh-tw.json
</pre>
把文件檔都放在 locales 下面,檔案名稱為:{title}.{lang}.json,英文的 guess.en.json 內容如下:<br />
<pre class="notranslate prettyprint">{
"en": {
"test" : "test",
"welcome" : "I have a number between %{low} and %{high}. Can you guess it?",
"guess_out": "Are you sure? I said a number between %{low} and %{high}",
"guess_unmatch" : "A number between %{low} and %{high}. Keep guess."
}
}
</pre>
對應的中文則是 guess.zh-tw.json:<br />
<pre class="notranslate prettyprint">{
"zh-tw": {
"test" : "測試字串",
"welcome" : "我有一個介於 %{low} 到 %{high} 的數字,你能猜到嗎?",
"guess_out": "你確定嗎?我說介於 %{low} 跟 %{high} 之間",
"guess_unmatch" : "介於 %{low} 跟 %{high} 之間,再接再勵"
}
}
</pre>
開頭是 language code,這邊這個名字要跟檔案的 {lang} 是相同的,雖然說有點多此一舉的感覺,內容則是 key-value 的形式儲存文字內容。<br />
<br />
主程式的部分也要對應的修改:<br />
<pre class="notranslate prettyprint lang-py">import i18n
i18n.load_path.append('locales')
i18n.set('file_format', 'json')
i18n.set('fallback', 'en')
language_code = data.get("queryResult").get("languageCode")
print(language_code)
i18n.set('locale', language_code)
text = i18n.t('guess.welcome', low = str(low), high = str(high))
</pre>
因為我們把檔案都放在 locales 下面,在搜尋路徑上加上 locales;檔案格式為 json;預設的語言是英文。<br />
在 assistant 的 request 裡面,語言設定會放在 queryResult -> languageCode 下,用 i18n.set 設定 locale;這時呼叫 i18n.t(title.key) 就會找出在 {title}.{lang}.json 檔案裡,{lang} 下面 key 對應的字串了,i18n.t 的參數,則可以代入預先設定好的 placeholder 代換到字串裡。<br />
<br />
測試一下,在測試頁面可以選擇測試的語言,選擇繁體中文來試試:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhN7KQsx41-T0KGY0VfU7rBKsQ8noul9QpNXB2kdzLODWOb6xN0i6KJ57G6WEOLCFEoipfGRphqvbAoUTvoogOJb95-wyOHU81yh676G1v3l_bYzldjcDnoCXlb3vYRMzPTf_Mi-XGvCUR/s1600/test.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhN7KQsx41-T0KGY0VfU7rBKsQ8noul9QpNXB2kdzLODWOb6xN0i6KJ57G6WEOLCFEoipfGRphqvbAoUTvoogOJb95-wyOHU81yh676G1v3l_bYzldjcDnoCXlb3vYRMzPTf_Mi-XGvCUR/s320/test.png" width="320" /></a></div>
<br />
現在我們的人工智障會講中文了。<br />
<br />
附註:在設定頁面全部可選的語言(依英文首字排序)有:繁中、粵、丹、荷、法、德、印度、印尼、義、日、韓、挪、波蘭、葡、俄、西、瑞典、泰、土;其中西、法、英三大家有口音選項。<br />
雖然在寫這篇的時候我有查到去年 12 月的新聞,說 google assistant <a href="https://venturebeat.com/2019/12/12/google-assistant-can-now-interpret-44-languages-on-smartphones/" target="_blank">會支援 44 種語言</a>,但截至我寫這個機器人的時候,還沒有這麼多的語言可以設定,可能是新聞跑得比開發工具快吧。另外 google assistant 的<a href="https://developers.google.com/assistant/sdk/reference/rpc/languages" target="_blank">說明文件</a>上,則是沒有這麼多種語言,可能開發工具又跑得比文件快一點。<br />
結論:宣傳 > 開發工具 > 開發文件(欸Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com2tag:blogger.com,1999:blog-2090850666133584855.post-36312853419882577872020-03-01T21:55:00.000+08:002020-03-03T01:14:48.956+08:00Rust std process 生成子行程最近在玩 Rust 的時候,需要用 Rust 去呼叫一些 shell command 來幫我完成一些事,幸好 Rust std 裡面已經有 <a href="https://doc.rust-lang.org/std/process/index.html" target="_blank">process</a> 來幫我們完成這件事,使用起來很像 python 的 subprocess,不過實際在用遇到一些問題,所以寫個筆記記錄一下:<br />
<br />
首先當然是從 std 引入這個模組的 <a href="https://doc.rust-lang.org/std/process/struct.Command.html" target="_blank">Command</a>,Stdio 很常用也順便 include 一下:<br />
<pre class="notranslate prettyprint lang-rust">use std::process::{Command, Stdio};
</pre>
<br />
一切的基礎就是一行 Command::new(command_name),在 command_name 的地方填入你想呼叫的指令。<br />
Command 代表了一個準備好要跑的命令,就像是在 shell 裡面打下 command_name 直接按 enter 一樣,沒有參數、繼續現在行程的環境、位置和現在行程的位置相同。<br />
如果要設定給命令的參數,就用 .arg 塞進去,如下面的例子:<br />
<pre class="notranslate prettyprint lang-rust">let mut ls = Command::new(ls).arg("-al");
</pre>
這個參數一次只能塞一個,有多個參數要連續呼叫 .arg 才行。<br />
<br />
有個 Command 之後接下來有三種方式讓它跑起來:.spawn(), .output(), .status():<br />
<br />
<ul>
<li>spawn fork 子行程執行,拿到一個子行程的 handler,回傳的型別是 Result<Child>。</li>
<li>output fork 子行程執行,等待(wait)它結束之後,收集它寫到 std output 的內容,回傳的型別是 Result<Output>。</li>
<li>status fork 子行程執行,等待它結束之後,收集它回傳的資訊,回傳的型別是 Result<ExitStatus>。</li>
</ul>
<br />
第一個可以注意到的是回傳的型別都是 Result,這是因為 command 可能會跑起來也可能會跑不起來,像是我打一個 Command::new("www") 但我的 shell 根本沒 www 這個指令,Result 提醒了這個可能性的存在,一般來說這邊最簡單的就是用 .expect 把 Result 解開。<br />
第二個另人疑惑的,是後面的 Child, Output, ExitStatus 是什麼鬼,整理之下大概是這樣:<br />
<br />
<ul>
<li><a href="https://doc.rust-lang.org/std/process/struct.ExitStatus.html" target="_blank">ExitStatus</a> 是最簡單的,就是行程結束的狀態的封裝,Rust 提供兩個介面 success 跟 code 來判斷子行程有沒有正常結束以及對應的 exit code。</li>
<li><a href="https://doc.rust-lang.org/std/process/struct.Output.html" target="_blank">Output</a> 是更上一層,裡面包了一層 status : ExitStatus,加上兩個 stdout, stderr 的 Vec<u8>,裡面存了子行程所有寫到 stdout 跟 stderr 的內容。</li>
<li>最外層就是由 <a href="https://doc.rust-lang.org/std/process/struct.Child.html" target="_blank">spawn</a> 產生的 Child,比起 output 跟 status 一生成行程就自動幫你 wait,spawn 給了完全的操作能力,可以做更多事情。</li>
</ul>
<br />
三個啟動的函式影響最大的就是子行程的 stdin/stdout/stderr,在 spawn 跟 status 下,stdin/stdout/stderr 會繼承父行程的 stdin/stdout/stderr;在 output 時,stdin 會被設定成不可使用(接到 /dev/null), stdout 跟 stderr 則會設定成 piped 來讀取。<br />
如果不想用預設的設定,可以在呼叫 status/output/spawn 前做設定,有三個選項可選 Stdio::inherit、Stdio::piped、Stdio::null,分別就是繼承父行程、接 piped 到父行程跟接上 /dev/null 。<br />
<br />
現在就能來玩一些例子,例如在 rust 裡面呼叫 ls,用 status() 的話輸出會直接輸出到螢幕上面:<br />
<pre class="notranslate prettyprint lang-rust">let p = Command::new("ls")
.arg("-al")
.status()
.expect("ls command failed to start");
</pre>
<div class="hl notranslate">
drwxr-xr-x 5 yodalee yodalee 4096 2月 29 09:45 .<br />
drwxr-xr-x 16 yodalee yodalee 4096 2月 28 20:10 ..<br />
-rw-r--r-- 1 yodalee yodalee 62279 2月 29 00:07 Cargo.lock<br />
-rw-r--r-- 1 yodalee yodalee 312 2月 29 00:07 Cargo.toml
</div>
<br />
如果想要把 ls 的內容截下來的話,就要改用 output:<br />
<pre class="notranslate prettyprint lang-rust">let p = Command::new("ls")
.arg("-al")
.output()
.expect("ls command failed to start");</pre>
這時候可以從 p.stdout 裡面拿到 Vec<u8>,要轉成字串就要用 <a href="https://doc.rust-lang.org/std/string/struct.String.html" target="_blank">String</a> 的 from_utf8/from_utf8_lossy/from_utf8_unchecked 函式轉。<br />
<pre class="notranslate prettyprint lang-rust">let s = from_utf8_lossy(&p.stdout);
println!("{}", s);
</pre>
<div class="hl notranslate">
drwxr-xr-x 5 yodalee yodalee 4096 2月 29 09:45 .<br />
drwxr-xr-x 16 yodalee yodalee 4096 2月 28 20:10 ..<br />
-rw-r--r-- 1 yodalee yodalee 62279 2月 29 00:07 Cargo.lock<br />
-rw-r--r-- 1 yodalee yodalee 312 2月 29 00:07 Cargo.toml</div>
<br />
如果要對子行程<strike>上下其手</strike>有完全的操控,就要使用 spawn 了,不過相對來說也要小心,因為 spawn 不會自動幫你 wait,不小心就會把子行程變殭屍行程。<br />
產生出來的 Child 物件,本身就自帶一些函式,像<br />
<br />
<ul>
<li>kill() 發 SIGKILL 把子行程砍了。</li>
<li>wait()、wait_output() 等待子行程結束,spawn + wait/wait_with_output 就相當於直接呼叫 status/output。</li>
</ul>
<br />
我們用 shell 的 rev 當作例子,它會輸入 stdin 反轉之後輸出,這裡不能用 output() 因為 output 的 stdin 不會打開;可以用 status ,這樣 stdin 會繼承本來的 shell 的 stdin 讓我們打字,但如果我們是要反轉程式裡面的一行字串呢?這時候我們就要用<strike> s.chars().rev().collect::<String>() 然後這篇文就不用寫了</strike> spawn 再操作 stdin 了。<br />
<br />
具體來說大概像是這樣:<br />
<pre class="notranslate prettyprint">let mut p = Command::new("rev")
.stdin(Stdio::piped())
.spawn()
.expect("rev command failed to start");
let stdin = p.stdin.as_mut().expect("Failed to open stdin");
stdin.write_all("Hello".as_bytes()).expect("Failed to write stdin");
</pre>
<br />
本來用 spawn 的話子行程的 io 會繼承父行程的,相當於上面那行改成 .stdin(Stdio::inherit()),這裡我們改用 Stdio::piped() 把它接出來。<br />
接著我們可以從 p (型別是 process::Child)裡去取得它的 stdin, stdout, stderr,這個拿到的都是 Option 型別,用 expect 把它給解開來,裡面就會拿到 Rust 的 io 物件,可以用呼叫對應<a href="https://doc.rust-lang.org/std/io/trait.Write.html" target="_blank">write 系列函式</a>對它寫入內容,這裡用 write_all 對 stdin 寫入 "Hello" 的 Vec<u8>。<br />
在 stdout 螢幕上就會看到 "olleH" 的輸出了。<br />
<br />
當然我們也可以在呼叫的時候把 stdout 也導向 piped 處理,讓我們讀出反轉的結果:<br />
<br />
<pre class="notranslate prettyprint lang-rust">let mut p = Command::new("rev")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("rev command failed to start");
let stdin = p.stdin.as_mut().expect("Failed to open stdin");
stdin.write_all("Hello".as_bytes()).expect("Failed to write stdin");
let output = p.wait_with_output().expect("Failed to read stdout");
let revs = String::from_utf8_lossy(&output.stdout);
assert_eq!(revs, "olleH");
</pre>
<br />
以上大概就是 Rust std process 使用方法的整理了,我自己大概有三點感想:<br />
<ol>
<li>用 Rust 寫其實沒有比 C 用 fork/exec 來寫來得簡單多少,畢竟我們就是要操作子行程,底層都是系統程式那套,Rust 頂多是封裝得比較完善一點,實際上用起來該設定的一個少不了。</li>
<li>要寫系統程式,系統程式的概念少不了,要寫 process 至少需要知道作業系統行程的概念(不然一不小心會變成 <strike>World War Z</strike> 殭屍產生器),操作輸入輸出需要大略知道 file descriptor 的概念,不然文件的繼承 stdin/stdout/stderr,piped 根本看不懂,不管你用哪套語言哪個作業系統,這些基本知識是逃不掉的。</li>
<li> 雖然如此,我覺得 Rust 仍然提供了一套不錯的封裝,在函式的回傳值上套用 Result/Option 的方式,能有效提醒使用者可能發生的錯誤,並要求使用者必須處理他們,這點我認為是花了差不多的成本之後,Rust 唯一可以勝過 C 的地方。</li>
</ol>
不小心寫了落落長,如果你竟然看到這行了,希望這篇文章對你有幫助XD。Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-17355100853034098592020-01-22T21:53:00.000+08:002020-07-13T01:12:17.231+08:00跨年不寂寞,讓 Google Assistant 陪你猜數字有了我們上次的 webhook 之後,我們可以真的來挑戰一些更複雜的<strike>助理</strike>智障功能,這次就來做跟你猜數字的助理,這樣就算跨年沒有朋友,還是有助理跟你玩有趣的猜數字遊戲,從螢幕感受到滿滿的溫暖 <img alt="" class="img" height="16" role="presentation" src="https://static.xx.fbcdn.net/images/emoji.php/v9/tf3/1.5/16/2764.png?_nc_eui2=AeH3VnjnQAM3-lpt51ZguIb8KRlZf293cGHOjUDLr_0AEQDar37megTB-K7evHa6pcYZSeSDXaaPJ6FV7y-xd_1Y25Cvs_v36_W095s6pgS2UQ" width="16" />(欸。<br />
<br />
其實做猜數字要做的事,在上篇的 webhook 中都差不多了。<br />
我們可以簡單畫一下流程圖,從開頭的 default welcome intent 開始:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPAfxIZLUEO5lJEv2BKHtMqr7YUcWrecXpYDXdzYDzxDPdz9IBAxw0gu2Lro-Zbb5xOPH70VTSf6gtdCKtVL8BgTQARTL9qGuMRlLeAxlHE3Pb_g7LhvWipe3RdNvpo94EvX1ih21_Ogsu/s1600/1workflow.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="305" data-original-width="652" height="149" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPAfxIZLUEO5lJEv2BKHtMqr7YUcWrecXpYDXdzYDzxDPdz9IBAxw0gu2Lro-Zbb5xOPH70VTSf6gtdCKtVL8BgTQARTL9qGuMRlLeAxlHE3Pb_g7LhvWipe3RdNvpo94EvX1ih21_Ogsu/s320/1workflow.png" width="320" /></a></div>
畫個簡單的流程圖是很重要的,特別是當設計的助理功能複雜到一定程度的時候,直接徒手下去硬幹很容易迷失在 intent 海洋中,特別是 dialogflow 的介面設計,在個別 Intent 中只能看到這個 Intent 會處理什麼輸入,給出什麼輸出,列出全體 Intent 的介面又看不出各 Intent 之間的關係,一下子就會迷失做亂掉,有了流程圖就能在編輯各 Intent 的時候,照著流程圖一一設定好。<br />
Webhook 也是,要在單一的 webhook 裡面處理所有的 Intent 該怎麼回應,當 Intent 數量一多的時候就會亂掉,所以都要事先做好規劃。<br />
<br />
簡單說一下上面的流程圖,default welcome intent 進來會產生一個數字,之後使用者輸入數字(猜數字),如果數字不對,會顯示更新的區間;對了就會顯示一句稱讚的話然後離開對話,讓我們開始實作:<br />
<ul>
<li>設定 Default Welcome Intent</li>
</ul>
這裡我們要回應一句請使用者猜數字的話,這句話簡單可以讓 dialogflow 自己回應就好,不過我們還是要打開 webhook,讓後端的伺服器產生一個亂數出來。<br />
在 webhook 的部分,如果偵測到 Intent 是 Default Welcome Intent,就用 random 產生一個亂數出來;另外要把這個 session id 跟亂數寫到資料庫裡,這個 session id 是固定的,同樣的 session id 就會對應到同一組對話。<br />
<ul>
<li>新增一個 Intent GuessNumber</li>
</ul>
這個 Intent 裡面可以設定一些例句,像是:<br />
<div class="hl notranslate">
I guess it is 11.<br />
Let me think. 25.<br />
38.<br />
Maybe 0.</div>
在 parameter 的地方把這個數字抽取出來,設定型別是 sys.number-integer,變數名number,這個 Intent 也要打開 webhook 讓伺服器處理使用者猜數字的行為。<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFtpoPifks9CpwtzF1qQzYCXWDtUTs8uhIMz33yla2lOGfv_9S5HGVWlDhPbDgvxZ5eD69eHO_fRRA3i0quNWdbIFKi2flgIxw4DqJDTWLD3JHPju0FDdCl-8q4S4vCFR2PcsP7DG1khE2/s1600/2guessintent.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFtpoPifks9CpwtzF1qQzYCXWDtUTs8uhIMz33yla2lOGfv_9S5HGVWlDhPbDgvxZ5eD69eHO_fRRA3i0quNWdbIFKi2flgIxw4DqJDTWLD3JHPju0FDdCl-8q4S4vCFR2PcsP7DG1khE2/s320/2guessintent.png" width="320" /></a></div>
<ul>
<li>新增一個 Intent GuessEnd</li>
</ul>
這個 Intent 不會由使用者的輸入進入,而是我們在 GuessNumber 的 webhook 設定 assistant 進入的狀態,在這裡要新增一個 Event,我叫它 User_number_match,在回應的部分設定一些恭禧使用者的話,然後設定這個 Intent 結束對話 End of Conversation。<br />
之所以要新增這個 event,是要讓 webhook 有能力讓 dialogflow 判定要進到這個 Intent,一般 Intent 的判定都是透過使用者的輸入來決定,但在猜數字裡面使用者輸入數字判定的 Intent 一定是 GuessNumber 不會是 GuessEnd,那對話就無法結束了。因此我們自定義這個 User_number_match 事件,只要 webhook 發出這個事件 dialogflow 就會判定為 GuessEnd Intent 了。<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh7n0X1I9I8PsWwxukwtualc6-15tyFLKh3dNezure4KXrmyOXXN7PoVKsPxAnGhNTpRd_F4bzyzcYtXFW5G4BIKRlxgtfP9kKWA0I2dI3Thxayo8PeZ3UIn5EJL3tLUeKTxxm0lKhxNNMo/s1600/3guessendintent.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh7n0X1I9I8PsWwxukwtualc6-15tyFLKh3dNezure4KXrmyOXXN7PoVKsPxAnGhNTpRd_F4bzyzcYtXFW5G4BIKRlxgtfP9kKWA0I2dI3Thxayo8PeZ3UIn5EJL3tLUeKTxxm0lKhxNNMo/s320/3guessendintent.png" width="320" /></a></div>
<br />
再來就可以寫 code 了,如上篇文所述,可以從送來的 json 中,從 queryResult -> intent -> displayName 拿到 Intent 的名字,用這個名字就能分派到不同的函式來處理;另外一個就是 json 的 session 可以拿到 session id。<br />
<pre class="notranslate prettyprint lang-python">session = data.get("session")
action_name = data.get("queryResult").get("intent").get("displayName")
</pre>
我的處理函式就是對三個出現的 Intent 去處理:<br />
Default Welcome Intent 產生亂數並寫入資料庫,這裡我是偷懶用 python 的 pickledb,雖然這樣推到 gae 上面可能會沒辦法用,但光為了這種小應用就要動用 gae 的 datastore 實在是有點大砲打小鳥,用 pickledb 展示一下概念就好了:<br />
<pre class="notranslate prettyprint lang-python">target = random.randint(low, high)
db.set(session, (low, target, high))
text = "I have a number between {} and {}. Can you guess it?".format(low, high)
reply = { "fulfillmentText": text }
return jsonify(reply)
</pre>
<br />
GuessNumber 的 webhook 會從資料庫裡面把存起來的數字拿出來,並從 queryResult/parameters/number 拿到使用者輸入的數字,雖然我的型別選擇 number-integer 了,dialogflow 還是塞了個 number float 給我,只能用 int 轉成 integer。<br />
後面就可以拿 guessnum 去跟 target 做比較,如果一樣的話就不會回覆 fulfillment 而是發送之前設定好的事件 User_number_match ,讓 dialogflow 進到 GuessEnd 並結束對話;不一樣的話就縮小可以猜的區間,設定回覆訊息給使用者。<br />
<pre class="notranslate prettyprint lang-python">minnum, target, maxnum = db.get(session)
guessnum = int(data.get("queryResult").get("parameters").get("number"))
if guessnum == target:
event = "User_number_match"
reply = { "followup_event_input" : { "name" : "User_number_match" } }
return jsonify(reply)
else:
# update minnum, maxnum here
db.set(session, (minnum, target, maxnum))
text = "A number between {} and {}. Keep guess.".format(minnum, maxnum)
reply = { "fulfillmentText": text }
return jsonify(reply)
</pre>
GuessEnd Intent 的 webhook 就很簡單,把 session id 對應的條目庫裡面刪掉就可以了。<br />
<br />
讓我們來測試一下:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhh7REqk5HWwr4q0fo9qNGI54hxaodSd9IsQ5J6wDX-dJghOvb8dqW5TGQdtSRugUh4T3E0BHXBrE8lkwR8otE2eXf4k6Nz9j8JFqmWP517z_99Bv3yLu2oSME8z5ZJshe07KDviMmg16Ne/s1600/4test.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhh7REqk5HWwr4q0fo9qNGI54hxaodSd9IsQ5J6wDX-dJghOvb8dqW5TGQdtSRugUh4T3E0BHXBrE8lkwR8otE2eXf4k6Nz9j8JFqmWP517z_99Bv3yLu2oSME8z5ZJshe07KDviMmg16Ne/s320/4test.png" width="320" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjC3KSJvm9-hMeqQopvHwlXLLauZ5HgWFvHv8Z7-yZNgrPNTQGQ_sSa5vj4Wn4FEROZ2hRPGNY_NeFZ-Nbxi-AcXNv3fCCttToUhaL0EvCR29mFLQzOHmQKhVZW9jhuQ8NTiVamDaPpYvuJ/s1600/5test2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjC3KSJvm9-hMeqQopvHwlXLLauZ5HgWFvHv8Z7-yZNgrPNTQGQ_sSa5vj4Wn4FEROZ2hRPGNY_NeFZ-Nbxi-AcXNv3fCCttToUhaL0EvCR29mFLQzOHmQKhVZW9jhuQ8NTiVamDaPpYvuJ/s320/5test2.png" width="320" /></a></div>
Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-3718395538239659292020-01-11T15:16:00.000+08:002020-07-13T01:12:17.231+08:00連接 Google Assistant Webhook<img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVAASnicWd82KbUeBzXdtcDnKqHIAISwjnUd6WmsEPLRSvkJAcjwrCr2jOnUwQgRnfv2wklUQqXB-zafeShYw4j2nGknWy-_zG13lPSG3uY4hYdm4i-CH_CZV8oBQN5AsSmtJYclAqUgIj/s1600/04_test.png" style="display:none" />
上一篇可以看到,我們的 action 可以從我們說的話裡面萃取出關鍵字詞,一般簡單的回應可以在 Intent 裡面剖析、回應,但 dialogflow 也僅止於判斷語意跟萃取關鍵字,如果使用者要使用外部服務,像是訂車票之類,一定要連接到外部訂票網站,這個時候就需要借助 webhook 的力量了。<br />
dialogflow 可以讓一個 Intent 的 fulfillment,也就是完成回應,送到另一個 server 的 webhook 來處理,由伺服器回應使用者的需求,同時間伺服器也能去呼叫其他的 API 服務,完成 Google Assistant 幫助使用者完成某件事情;這個做法的優先權比較高,我試過用了 webhook ,它的回應會蓋過 Intent 裡面設定的回應,完整的介紹可以參考 <a href="https://cloud.google.com/dialogflow/docs/fulfillment-overview" target="_blank">Google 的文件</a>。<br />
<br />
如果有看 codelab 的課程,裡面使用的回應是用 dialogflow 內建的 server 或是連接到 firebase 上面的 server,兩個都是用 nodejs 實作,這是我們第一個要解的問題:我不想寫 nodejs 看到 nodejs 就會傷風感冒頭痛發燒上吐下瀉四肢無力,所以我們不能用 nodejs。<br />
<div>
幸好隨手拜 google 大神,就找到有人<a href="https://medium.com/zenofai/creating-chatbot-using-python-flask-d6947d8ef805" target="_blank">用 python 架 server</a>,之前在 MOPCON 講 COSCUP chatbot 的講者大大也是<a href="https://hackmd.io/@mopcon/2019/%2F%40mopcon%2FryFCOLxYB" target="_blank">用 golang 架 server</a>,所以要擺脫 nodejs 一定是沒問題。<br />
<br />
我們這篇的目標是做一個把 python server 給架起來,然後把歡迎訊息改成用 webhook來回應,都是 google 的服務,伺服器可以用同專案開 gae 放在上面,但測試時用 ngrok 本地測試會比較方便。<br />
首先是先設定 webhook,在 dialogflow 裡面把 Default Welcome Intent 最下面 fulfillment 的 Enable webhook call for this intent 打開:</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGG4Xlarnxul0NMZw1htcNof3Yg1O9-8MxF-LMhyVXLQ8SWhxjrNPdOiVze7oTJXVfovHmIAQ9hsBqz9Nvt6tHmB2MaxpyjvWyC6NzQfk6zvmZ1gOPSRLQAKcNzMJOvpwDumqXT1zxMtnQ/s1600/03_openwebhook.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em; text-align: center;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGG4Xlarnxul0NMZw1htcNof3Yg1O9-8MxF-LMhyVXLQ8SWhxjrNPdOiVze7oTJXVfovHmIAQ9hsBqz9Nvt6tHmB2MaxpyjvWyC6NzQfk6zvmZ1gOPSRLQAKcNzMJOvpwDumqXT1zxMtnQ/s320/03_openwebhook.png" width="320" /></a></div>
<div>
<br /></div>
<div>
在 fulfillment 裡面打開 webhook,在 URL 裡面填上 webhook 的位址,這部分等等寫好服務之後再來填:</div>
<div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJSWlsh7Oq4RYCsRvjMIKiKLeAAE6B7hkyE4gn26ZxVSs3_PBsMtG_B318T_MyI8OjADVhk699f2A1Pzco0vDkwdKcjNHBfVNwLdl7oPM9KS6wsxArJSKycveFQ4x9dgtD0dp2U4V_dPMm/s1600/02_fulfillment.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em; text-align: center;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJSWlsh7Oq4RYCsRvjMIKiKLeAAE6B7hkyE4gn26ZxVSs3_PBsMtG_B318T_MyI8OjADVhk699f2A1Pzco0vDkwdKcjNHBfVNwLdl7oPM9KS6wsxArJSKycveFQ4x9dgtD0dp2U4V_dPMm/s320/02_fulfillment.png" width="320" /></a></div>
<div>
<br /></div>
下面開始寫我們首先開一個新的 python 專案,建立 pip requirements.txt:<br />
<div class="hl notranslate">
# requirements.txt<br />
Flask==1.1.1</div>
<br />
使用 pip 跟 virtualenv 建立環境:<br />
<div class="hl notranslate">
$ python -m venv env<br />
$ source env/bin/activate<br />
$ pip install -r requirements.txt</div>
<br />
接著建立 flask 實作的 webhook:<br />
<pre class="prettyprint notranslate lang-py">from flask import Flask, request, jsonify, make_response
import json
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
data = request.get_json(silent=True, force=True)
print("Request:{}".format(json.dumps(data, indent=2)))
action_name = data.get("queryResult").get("intent").get("displayName")
if action_name == "Default Welcome Intent":
text = "Welcome to my google assistant"
reply = { "fulfillmentText": text }
return jsonify(reply)
</pre>
下面是一個 Default Welcome Intent 的 webhook request,要看這個內容可以使用 dialogflow 右手邊的 Try it now 搭配 diagnostic info,可以確認 dialogflow 在判斷 Intent 有沒有錯誤,還有 fulfillment request ,也就是送到 webhook 的內容。<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCCRU9XyAPosvVAHP202TXlnkSEThvaAsB_iTdEfLylmt0i_21g-39PZP-vErjiNcE_i8NcY1kDRIP-BqEYy8Db7YLJSZvg8x0fMTO_0oOcQN2YAEZsLoLEq-DTXPtE-LkKzUl7pd38KM8/s1600/00_dialoginfo.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCCRU9XyAPosvVAHP202TXlnkSEThvaAsB_iTdEfLylmt0i_21g-39PZP-vErjiNcE_i8NcY1kDRIP-BqEYy8Db7YLJSZvg8x0fMTO_0oOcQN2YAEZsLoLEq-DTXPtE-LkKzUl7pd38KM8/s320/00_dialoginfo.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2YDWmgPNjhPCpwegkf1PCysMi8zvBJNNvgtDWcgLqr0LpA-r-DUQcIetODltxZpQmcNjAsHxzN3N60QhyphenhyphenNjlE1NOoeU8Ae78Xuqc01W_2ILAR5iT67klj5uG-83ngdw6_m_hEG_XjdzvK/s1600/01_diagnosticinfo.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2YDWmgPNjhPCpwegkf1PCysMi8zvBJNNvgtDWcgLqr0LpA-r-DUQcIetODltxZpQmcNjAsHxzN3N60QhyphenhyphenNjlE1NOoeU8Ae78Xuqc01W_2ILAR5iT67klj5uG-83ngdw6_m_hEG_XjdzvK/s320/01_diagnosticinfo.png" width="320" /></a></div>
<br />
下面節錄我 welcome 訊息的 request:<br />
<ul>
<li>session:一個對話的 id,每一輪的話都會是同一個 id,作為對話的識別:</li>
<li>queryResult queryText:對話的內容</li>
<li>queryResult intent displayName:目前 dialogflow 判定使用者的意圖</li>
</ul>
dialogflow 接受的回應內容是 json 格式,可以填充的內容請見<a href="https://cloud.google.com/dialogflow/docs/reference/rpc/google.cloud.dialogflow.v2?hl=zh-tw#webhookresponse" target="_blank">參考文件</a>,最簡單的一個回應就是設定 key 為 fulfillmentText 的內容,這個內容就會是 Google Assistant 要顯示給使用者的回應。<br />
<br />
最後我們使用 <a href="https://ngrok.com/" target="_blank">ngrok</a> 來進行測試,ngrok 是一個網路服務,幫你把連接到 ngrok 的連線重導向到 localhost。在使用 ngrok 之前,測試架在雲端的網路服務流程會像這樣:<br />
寫程式;在 local 進行有限的測試;上傳到雲端(等等等);跑了之後發現 server 炸掉;去雲端上面撈 log 檔(等等等);改完之後所有步驟重複一次。<br />
用了 ngrok 之後,程式在本地、log 檔也在本地,上述耗時又麻煩的上傳雲端、撈 log 檔都省下來,真的是瞬間人生變成彩色的。<br />
<br />
使用 ngrok 也非常簡單,安裝好 ngrok 之後照著網頁的指示先註冊金鑰:<br />
<div class="hl notranslate">
$ ngrok authtoken <token><br />
$ ngrok http 8080<br />
Forwarding https://wwwwww.ngrok.io -> http://localhost:8080</div>
<br />
也就是 wwwwww.ngrok.io 已經被映射到我們的 localhost:8080 由 flask 執行的伺服器,因此我們可以在 webhook 的地方填入 wwwwww.ngrok.io/webhook。<br />
<br />
我們來測試一下:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVAASnicWd82KbUeBzXdtcDnKqHIAISwjnUd6WmsEPLRSvkJAcjwrCr2jOnUwQgRnfv2wklUQqXB-zafeShYw4j2nGknWy-_zG13lPSG3uY4hYdm4i-CH_CZV8oBQN5AsSmtJYclAqUgIj/s1600/04_test.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVAASnicWd82KbUeBzXdtcDnKqHIAISwjnUd6WmsEPLRSvkJAcjwrCr2jOnUwQgRnfv2wklUQqXB-zafeShYw4j2nGknWy-_zG13lPSG3uY4hYdm4i-CH_CZV8oBQN5AsSmtJYclAqUgIj/s320/04_test.png" width="320" /></a></div>
<br />
看到我們的 Assistant 回應了我們在 webhook 設定的回應。</div>
Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-85086428972928206292020-01-04T14:11:00.002+08:002020-07-13T01:12:17.231+08:00跨年好寂寞?使用 Google Assistant 跟你對話<img border="0" data-original-height="847" data-original-width="1600" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgEkknwC7UiA7DevCJaU7_golxLyzyRlaow_qbBuZugcPcgJXJ2NgZEXc_fjdalb07xfg1J1kt39J1fjyVIqTkfSqGRlnsmmy8Vr-F6acTfzgKMYs92ZZXyexDqmLo_nTlhA1H1xawHyfMP/s1600/07_test.png" style="display: none;" />
故事是這樣子的,2019-2020 的跨年,小弟邊緣人沒地方去,後來就自己回家當紅白難民,然後還沒聽到 Lisa 唱紅蓮華QQQQ,不過幸好宅宅有宅宅的做法,沒聽到紅蓮華我們可以看 Youtube <a href="https://www.youtube.com/watch?v=tnIHWvf-vgE" target="_blank">別人上傳的影片</a>,沒有朋友跨年我們可以自幹朋友,也就是我們今天的主角:Google Assistant。<br />
如果平常有用 Google Pixel 手機,或是家裡有 Google Home 裝置的,應該就會知道它上面附的 Google Assistant <strike>Christina</strike>,雖然說我覺得還是滿沒用的啦,我自己只會拿它來查天氣,有 Google Home 的同學只用它來開關燈,但反正,都有這麼好的工具了為什麼不來好好玩一下?就趁著跨年的假日做點不一樣的事來玩。<br />
<br />
下面是一個基礎的流程,跟 <a href="https://codelabs.developers.google.com/" target="_blank">codelabs</a> 的課程(搜尋 google assistant 有三級課程可以上)一樣,做一個會回應你的小程序,我們想做到的就是:它會問你最喜歡的顏色,然後會重複你的話:你最喜歡的顏色是XXX。<br />
<br />
首先來到 <a href="https://console.actions.google.com/" target="_blank">google action </a>的頁面(<strike>雖然是 Google Assistant 卻叫 Google Action 呵呵</strike>)先建立一個新的 project,類型選擇對話 Conversational,語言我是建議先選英文,之後應該會試著做做看中文的助手,但英文比較萬無一失。<br />
建立之後要設定一個發語詞,平常在手機上呼叫 Google Assistant 是用 OK google,另外也可以用 talk to my "發語詞" 來呼叫你寫的程式,或者是打開某個 App,這裡我們沒決定名字就叫它 TestApp 就好。<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiVkNyekdWQVfcF5_xHds_bSNSyvTd2e8Go7A1SsbKJgnS6d4WUwDNUOan7ebwZvyHXi_DmdKmUHVKqWe8sX-E_yQ_3OrM3JPAbDeUIQPfgUyuhg9H36GFu2Hux2I0VQ-_T6kVDikjN4rPL/s1600/01_actiondevelop_quicksetup.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiVkNyekdWQVfcF5_xHds_bSNSyvTd2e8Go7A1SsbKJgnS6d4WUwDNUOan7ebwZvyHXi_DmdKmUHVKqWe8sX-E_yQ_3OrM3JPAbDeUIQPfgUyuhg9H36GFu2Hux2I0VQ-_T6kVDikjN4rPL/s320/01_actiondevelop_quicksetup.png" width="320" /></a></div>
<br />
下一步要產生一個 Action,選擇 Custom Intent 再點 Build ,這會連結你的 action 到 dialogflow 建立一個新的 Agent,可以想像一個 Agent 就是回話的機器人,這裡一樣語言建議選擇英文,時后就選擇 +8 時區(不知道為什麼只有香港可選)。<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgs2Py1bGMO7cXuexYprpeYa2_QEQZum6iu7oJuiPV2OPIKPyBaARv-VqfEhi3ZXPqpiOdzNPUxoAjtroCfBQl9EZVP_Lvn812MJwnkgZqpc8OlqAegNVMu9RAuGeQ7NwTbiJSeDdmp2_ED/s1600/02_dialogflow.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgs2Py1bGMO7cXuexYprpeYa2_QEQZum6iu7oJuiPV2OPIKPyBaARv-VqfEhi3ZXPqpiOdzNPUxoAjtroCfBQl9EZVP_Lvn812MJwnkgZqpc8OlqAegNVMu9RAuGeQ7NwTbiJSeDdmp2_ED/s320/02_dialogflow.png" width="320" /></a></div>
<br />
這個背後流程是這樣子的,你跟 Google Assistant 講話之後,Google Assistant 會把這段話送到 Google Action,那 Google Action 又要怎麼理解這段話?就是靠 dialogflow 服務,算是一個簡化版本的自然語言理解框架,可以理解說話的意圖,解析出關鍵字送出回應,而中間這些關鍵字跟回應是可以由設計者設定的;其他家類似的服務像是 Amazon Lex、IBM Watson 等。<br />
dialogflow 由許多的 Intent(意圖)所構成,dialogflow 會從 google assistant 來的輸入辨識出現在要選擇哪個意圖,然後照著意圖的設定去回應;可以把google assistant 想像成一個狀態機,意圖想成一個狀態,照使用者的輸入進到不同狀態,依狀態決定輸出內容,以及下一個可能的狀態。<br />
<br />
預設一定有的意圖就是 Default Welcome Intent,也就是一啟動 google assistant 時的的狀態,我們在它的 Response 裡面加上回應:"What is your favorite color?",這樣程式一開就會把問題丟給使用者。<br />
這句話非常的重要,<b>設計 chatbot 最重要的就是把使用者限制在一個小框框裡面,讓使用者針對只有有限選項或是只能答 yes/no 的問題做回答</b>,否則使用者天馬行空,<請你跟我結婚>、<誰是世界上最美麗的女人>這種問題滿天飛,結果你的 chatbot 完全聽不懂,畢竟人人都以為人工智慧可以做到穿熱褲黑絲襪又會拌嘴的傲嬌助手,實際上還不是血汗工程師在後面拉線設定的人工智障,隨便一問就被看破手腳。<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhRK6Cn9tG_XLOrlXd6z4XOBQaA5U0qOduEBeC3OYAehL8vAC31e97XS4tTWwyqRPQHA4h_90ouIjvNeGs-IkmDRS2liCTjM4fKk19xGQnZLsXbJUb1kNDqcH1kSLQ1H_FbJsaZSxdR2V7i/s1600/04_default.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhRK6Cn9tG_XLOrlXd6z4XOBQaA5U0qOduEBeC3OYAehL8vAC31e97XS4tTWwyqRPQHA4h_90ouIjvNeGs-IkmDRS2liCTjM4fKk19xGQnZLsXbJUb1kNDqcH1kSLQ1H_FbJsaZSxdR2V7i/s320/04_default.png" width="320" /></a></div>
<br />
另外我們要新增一個 Intent ,命名為 favorite color,注意這個命名在未來連接 webhook 時非常重要,命名跟大小寫都要注意。<br />
在 training phrase 的地方,試著打入一些使用者可能會說的話,最好是上一句問句的回話:<br />
<div class="hl notranslate">
blue<br />
my favorite color is red<br />
orange is my favorite<br />
ruby is the best</div>
邊打的時候 dialogflow 就會自動的把顏色部分給標起來,接著往下拉到 Action and parameters ,系統應該會自動加上一個 color 的 parameter,我們設定 entity 為 @sys.color,value 為 $color。<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMQCx6Jt2WTJ-9vjTu577GmFFmhJGsU1q9HaHp_YP18DNsWtP-9-TWqchVN-Y-vUXWRT3qAmcxC0HSMCpHfsq-yS2GEJd2G81nhAnsZqw21xGxiJkCeHyhTcGzAYVzsh8J-CBlgrm5o6ly/s1600/05_training.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMQCx6Jt2WTJ-9vjTu577GmFFmhJGsU1q9HaHp_YP18DNsWtP-9-TWqchVN-Y-vUXWRT3qAmcxC0HSMCpHfsq-yS2GEJd2G81nhAnsZqw21xGxiJkCeHyhTcGzAYVzsh8J-CBlgrm5o6ly/s320/05_training.png" width="320" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhN_bBJvdmCLtwwCgT3HYfz_0QkVtsb-9oA4cXDwBB54QBozsOtdh29zj4FjeOahtHOFk6402RO4DnUIgtygcVmbeJZ5SR9EqGG_039DEEcb75m5tTm4_NB4DFQglWUkImW0Y8Y4c77U0vV/s1600/06_response.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhN_bBJvdmCLtwwCgT3HYfz_0QkVtsb-9oA4cXDwBB54QBozsOtdh29zj4FjeOahtHOFk6402RO4DnUIgtygcVmbeJZ5SR9EqGG_039DEEcb75m5tTm4_NB4DFQglWUkImW0Y8Y4c77U0vV/s320/06_response.png" width="320" /></a></div>
<br />
這裡就能看出 dialogflow 服務的功力了,在我們打上例句的時候 dialogflow 就透過事先分類好的 entity,分析出我們現在想要知道使用者回話的什麼內容(這裡是顏色),再幫我們把這個內容存到變數裡。<br />
最後在 response 的地方,我們使用剛剛萃取出來的內容:<br />
<div class="hl notranslate">
OK. Your favorite color is $color</div>
這樣就完成我們的回話機器人了,當然我們要測試一下,在 Integration 選擇 integration setting,記得打開 Auto-preview changes 後再點 test,就可以在 google action 測試頁面中測試了:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgEkknwC7UiA7DevCJaU7_golxLyzyRlaow_qbBuZugcPcgJXJ2NgZEXc_fjdalb07xfg1J1kt39J1fjyVIqTkfSqGRlnsmmy8Vr-F6acTfzgKMYs92ZZXyexDqmLo_nTlhA1H1xawHyfMP/s1600/07_test.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="847" data-original-width="1600" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgEkknwC7UiA7DevCJaU7_golxLyzyRlaow_qbBuZugcPcgJXJ2NgZEXc_fjdalb07xfg1J1kt39J1fjyVIqTkfSqGRlnsmmy8Vr-F6acTfzgKMYs92ZZXyexDqmLo_nTlhA1H1xawHyfMP/s320/07_test.png" width="320" /></a></div>
<br />
短短小文祝大家新年快樂,希望大家都有個機器人陪你過年(欸Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-71516830600895539672019-12-14T12:38:00.000+08:002019-12-14T23:46:47.043+08:00用 Qt Graphics 做一個顯示座標的工具 - 細節調整其實這篇才是我寫文的主因,前面兩篇其實都是前言(欸),總之就是我埋頭自幹 AZ 大大吃一頓飯就寫得出來的工具,結果過程中寫得亂七八糟,有時候該出現的東西就是不出現測到頭很痛,或是測一測就進無窮迴圈後來發現是自己蠢,發了 v0.1 之後被靠北顯示怎麼這麼醜,想改又很難搜不到解法QQ。<br />
當然最後還是搜到了,但就希望可以多寫一篇文,讓這些解法更容易被搜到,如果運氣很好真的幫到人也算功德一件,<strike>如果你真的被幫到的話,麻煩幫我這篇文章留下一個 like 然後順手按下旁邊的訂閱和小鈴鐺</strike>(醒醒這裡是 blogger 不是 youtube。<br />
<h4>
畫上座標線:</h4>
這算是一個附加功能,一般的顯示工具只要設定 Scene 的 <a href="https://doc.qt.io/qt-5/qgraphicsscene.html#backgroundBrush-prop" target="_blank">background brush</a>,設定一個黑色的 brush,就能畫出黑色背景了。<br />
但如果我們要更精細的話,就要去實作 scene 或是 view 的 <a href="https://doc.qt.io/qt-5/qgraphicsscene.html#drawBackground" target="_blank">drawBackground</a> 函式:<br />
<pre class="notranslate prettyprint lang-c">void drawBackground(QPainter *painter, const QRectF &rect)
</pre>
<br />
下面是我實作的程式碼節錄,有幾個可以注意的地方:<br />
<ol>
<li>Qt 的座標系統左上角是 0,0,往右往下是 x 遞增跟 y 遞增,所以用 top 的值會比 bottom 小。</li>
<li>這個函式參數是一個 QPainter 跟一個 QRectF,painter 好理解就是目前作畫的對象,在這個 painter 上面作畫就會畫在背景上;QRectF 一般會認為就是現在顯示背景的矩形,不過不對,它是「現在要更新背景的範圍的矩形」。<br />例如我現在顯示的 Scene 大小是 -50,-50 ~ 50, 50,但我實作滑鼠可以在 scene 上面拉一個選取的小框框,在我拉框框的時候,Qt 會判定只有這個小框框裡面的背景是需要重畫的,QRectF 就會設定到這個小框框上;其實某種程度來看這樣也對,而且更節省重畫的資源。</li>
<li>承上點,所以有抓到現在 scene 的大小要怎麼辦?這也是我為什麼選擇是實作 View 的 drawBackground 而不是 Scene 的,因為我可以透過 mapToScene(viewport()->rect()).boundingRect() 取得這個 View 現在顯示 Scene 的大小。</li>
</ol>
後來就是一些數學計算,在對應的座標點上呼叫 drawPoint 了,下面的 code 我有略為刪節過了,要用的話不要全抄。<br /><ol>
</ol>
<pre class="notranslate prettyprint lang-c">void
MyViewer::drawBackground(QPainter *painter, const QRectF &rect) {
qreal left = rect.left();
qreal right = rect.right();
qreal top = rect.top();
qreal bottom = rect.bottom();
QRectF sceneRect = mapToScene(viewport()->rect()).boundingRect();
qreal size = qMax(sceneRect.width(), sceneRect.height());
qreal step = qPow(10, qFloor(log10(size/4)));
qreal snap_l = qFloor(left / step) * step;
qreal snap_r = qFloor(right / step) * step;
qreal snap_b = qFloor(bottom / step) * step;
qreal snap_t = qFloor(top / step) * step;
// print coordinate point
for (qreal x = snap_l; x <= snap_r; x += step) {
for (qreal y = snap_t; y <= snap_b; y += step) {
painter->drawPoint(x, y);
}
}
// print coordinate line
painter->drawLine(qFloor(left), 0, qCeil(right), 0);
painter->drawLine(0, qCeil(bottom), 0, qFloor(top));
QGraphicsView::drawBackground(painter, rect);
}
</pre>
<h4>
不動物件:</h4>
第一個是所謂的不動物件,也就是 QGraphicsItem 透過設定了 <a href="https://doc.qt.io/archives/qt-4.8/qgraphicsitem.html#GraphicsItemFlag-enum" target="_blank">ItemIgnoresTransformations flag</a>,這樣這個 item 就不會受到 view 視角變化的影響。<br />
<br />
使用情境也很單純,像是在畫面上打個 marker 或是寫上文字,如果視窗縮小就看不到就奇怪了,所以這個 marker 就要設定這個 flag,放大縮小都會顯示一樣。<br />
改變也就是呼叫一下<br />
<pre class="notranslate prettyprint lang-c">setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
</pre>
就可以了。<br />
<br />
要注意的是在設定了這個 flag 之後,在這個 item 裡面的位移似乎會失去效果(還是行為會變很怪,我有點忘了),一般要在一個位置例如 100, 100 畫一個正方形,我們可以用 QGraphicsRectItem,在 100, 100 的地方畫正方形;如果是 ignore transformation 的物件,我是變成在 0,0 的位置畫一個正方形,然後把物件的位置用 setPos 設定在 100, 100。<br />
這部分當初真的弄超久,後來覺得這樣不行,把放在 dropbox 裡面的 <c++ GUI Programming With Qt 4> 拿出來翻翻,沒想到在第八章 Qt graphics 章節就講了要怎麼寫類似的東西,還有範例 code ,果然寫程式還是要多看書而不是瞎攪和,弄了好一陣子的東西其實書上的範例都寫了。<br />
<br />
<h4>
填充物:</h4>
上一篇文的最後一張圖,應該很明顯可以看到,我用 brush 填進去的東西,非常的…不均勻,一格一格的非常醜,實際運作的 code 也是,只要放大縮小填充物的就會變得不連續。<br />
這是因為 QBrush 在填東西的時候,用固定密度在填充,不會隨著螢幕的放大縮小改變填充物的密度,要修成也只需要一行,在 paint 函式裡面加上這個:<br />
<pre class="notranslate prettyprint lang-c">QBrush m_brush;
m_brush.setTransform(QTransform(painter->worldTransform().inverted()));
painter->setBrush(m_brush)
</pre>
把現在場景的變形反轉補償回去就可以了;這個解答出自 <a href="https://stackoverflow.com/questions/13958385/how-to-make-qt-qgraphicsview-scale-to-not-affect-stipple-pattern" target="_blank">Stack Overflow</a>。<br />
<br />
<h4>
隨放大縮小調整長度:</h4>
同樣的是另一張圖的箭頭,在放大縮小的時候,箭頭的部分會跟著放大縮小,這是我們不想要的,因為縮太小的時候箭頭會看不到,這時候就要用到我們上篇提到的 QStyleOptionGraphicsItem,在 paint 函式裡面,可以用這個東西從 painter 的 transform 裡取得 level of detail (LOD):<br />
<pre class="notranslate prettyprint lang-c">qreal qScale = option->levelOfDetailFromTransform(painter->worldTransform());
qreal len = 10 / qScale;
</pre>
qScale 就是目前放大值,可以用它調整我們要畫的長度 len;這個解答出自 <a href="https://kheresy.wordpress.com/2011/10/07/%E5%BB%BA%E7%AB%8B%E4%B8%80%E5%80%8B%E4%B8%8D%E8%A2%AB-view-%E5%BD%B1%E9%9F%BF%E7%B7%9A%E6%A2%9D%E5%AF%AC%E5%BA%A6-graphics-item/" target="_blank">Heresy's Space</a>。<br />
<br />
下面列一下參考書目:<br />
<ul>
<li>C++ GUI Programming With Qt 4</li>
<li>Game Programming using Qt 5 Beginner's Guide: Create amazing games with Qt 5, C++, and Qt Quick</li>
</ul>
<br />Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-70308581364730707362019-12-11T23:16:00.001+08:002019-12-14T23:46:47.031+08:00用 Qt Graphics 做一個顯示座標的工具 - 客製化元件上一篇我們介紹了 Scene, View, Item 的關係,這篇就來客製化一下,畢竟 Qt 的元件沒客製化功能都非常受限,預設行為幾乎什麼都沒有,這時候就是好好重溫 C++ 最美妙功能––繼承的時候了。<br />
首先是 Scene 跟 View,都用繼承的方式建一個自己的 class,才能在裡面實作各種信號跟插槽,設計上 Scene 是 View 的 data member,View 上面接到什麼東西直接 pass 給 Scene,實作就不一一介紹,下面是一個簡單的修改列表,因為是內部用的工具就沒辦法把程式碼貼上來<strike>給大家笑了</strike>:<br />
<br />
View實作事件:<br />
<br />
<ul>
<li>keyPressEvent:客製化按鍵盤的行為。</li>
<li>wheelEvent:連接到放大縮小的函式。</li>
</ul>
<br />
<br />
View實作插槽:<br />
<br />
<ul>
<li>clearScene:清空畫面</li>
<li>addItem:接收讀進來的物件,往下直接呼叫 Scene 的 addItem。</li>
<li>zoomToAll 跟 zoomRect:放大縮小。</li>
</ul>
<br />
<br />
Scene 實作事件:<br />
<br />
<ul>
<li>mousePressEvent/mouseMoveEvent/mouseReleaseEvent:定義滑鼠行為。</li>
</ul>
<br />
<br />
Scene 實作信號:<br />
<br />
<ul>
<li>rectSelected:滑鼠事件會發出這個信號,通知 View 跟 MainWindow。</li>
<li>mouseClick:同樣用來通知 MainWindow 使用者點在哪裡。</li>
</ul>
<br />
<br />
當然我們也要客製化自己的 <a href="https://doc.qt.io/archives/qt-4.8/qgraphicsrectitem.html" target="_blank">QGraphicsItem</a>,當然如果要顯示的東西沒太多特別要求,只靠 Qt 提供的那些 QGraphicsXXXItem 也是 OK 的。<br />
我記得在實作時候也有考慮過是不是繼承 <a href="https://doc.qt.io/archives/qt-4.8/qabstractgraphicsshapeitem.html" target="_blank">QAbstractGraphicsShapeItem</a> 就好,後來好像因為什麼原因,還是繼承 QGraphicsItem。<br />
<br />
照著 Qt 的<a href="https://doc.qt.io/qt-5/qgraphicsitem.html#details" target="_blank">說明文件</a>,要實作自己的 QGraphicsItem,重點在打造兩個函式:<br />
<pre class="notranslate prettyprint lang-c">QRectF boundingRect() const override
</pre>
這個函式要回傳一個 QRectF,標示這個 Item 的大概位置,讓 Scene 能用它作分類跟檢索,如果設錯的話就有可能變成 item 明明在某個位置視窗卻打死不顯示它,因為 Scene 在檢索的時候就不認為這個 Item 有需要顯示。<br />
<pre class="notranslate prettyprint lang-c">void paint(
QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override
</pre>
Paint 就是操作 Painter 裡面的函式盡情的作畫,不過我自己是省得麻煩,都是創一個 QPainterPath 然後呼叫 <a href="https://doc.qt.io/qt-5/qpainter.html" target="_blank">Painter</a> 的 drawPath,這樣比較簡單。<br />
舉例來說我自己實作的一個物件是在視窗上面標上一個箭頭然後顯示文字,大略來說程式碼就是這樣:<br />
<pre class="notranslate prettyprint lang-c">QPainterPath path;
path.moveTo(0, 0);
path.lineTo(0, 8);
path.lineTo(5.656, 5.656);
path.closeSubpath();
path.lineTo(7.65, 18.48); // len 20, tilt 22.5
path.setFillRule(Qt::WindingFill);
QFont serif("Helvetica", 12);
path.addText(QPoint(0, 0), serif, m_text);
</pre>
paint 裡再用 drawPath 把這條 path 畫出來就可以了。<br />
<br />
Paint 的另外兩個參數 QStyleOptionGraphicsItem 和 QWidget,前者帶著顯示上要用的參數,在下篇做一些細部設定的時候會用到;後者則指向目前繪圖中的物件,通常可以不用管它,我也還不確定什麼時候會用到它。<br />
這次實作全面採用 C++ 的新關鍵字 override,個人認為真的好用,像是上面的 boundingRect 如果沒寫 const 的話其實是在實作不同的函式,加了 override 編譯器就會跳錯誤,比較不會犯這種不容易注意到的錯。<br />
<br />
實作 QGraphicsItem 以我們的應用這樣就夠了,如果還需要更精細的管理,可以再實作函式:<br />
<pre class="notranslate prettyprint lang-c">QPainterPath shape() const
</pre>
這個函式用來檢查碰撞、滑鼠有沒有點在物件上等等,不實作預設就是以 boundingRect 代替。<br />
<br />
自己幹完 QGraphicsItem 之後,整個程式也有了個樣子了,繼承自 QGraphicsItem 的物件可以直接用 addItem 塞進 Scene 裡面,下面截兩張運作起來的樣子,分別是顯示箭頭跟一個三角形的多邊形:<br />
<br />
<div class="separator" style="clear: both; text-align: left;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiigMaVvFEko4Fk9MZSbYmYtDgAF-JQl8wAPG5X-RPxbHfrYP10OsQxaZRqxoG8FokdXwmChVB-QYogv2r3aWhyphenhyphenEQwDewz0_vJYnjfDNe8ZjSSbOWif5Mtt0iG_WvIn1aS6m_v2lW1SEbEP/s1600/arrow.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="460" data-original-width="652" height="225" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiigMaVvFEko4Fk9MZSbYmYtDgAF-JQl8wAPG5X-RPxbHfrYP10OsQxaZRqxoG8FokdXwmChVB-QYogv2r3aWhyphenhyphenEQwDewz0_vJYnjfDNe8ZjSSbOWif5Mtt0iG_WvIn1aS6m_v2lW1SEbEP/s320/arrow.PNG" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgpPk9xnXcy8bJZe1VRfO5rofGaC8fbhJ3v6_p5IpPkAt5TuePYwrY9UVfwPzK48eJiNtKvE3MEtG93mHiI_s9Pyo-OHGaHQnmdN3gpaco906hsDxyUG2WNvR47RsZUYjYrt8eR3Yb753BU/s1600/polygon.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="479" data-original-width="651" height="234" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgpPk9xnXcy8bJZe1VRfO5rofGaC8fbhJ3v6_p5IpPkAt5TuePYwrY9UVfwPzK48eJiNtKvE3MEtG93mHiI_s9Pyo-OHGaHQnmdN3gpaco906hsDxyUG2WNvR47RsZUYjYrt8eR3Yb753BU/s320/polygon.PNG" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
還有很多細部設定沒做所以看起來會有一點粗糙,下一篇預定就是要講這些細部的東西。</div>
Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-78911428548510562022019-12-05T00:08:00.001+08:002019-12-14T23:46:47.082+08:00用 Qt Graphics 做一個顯示座標的工具故事是這樣的,平常小弟在公司處理的東西多半是一些 polygon, line 之類的資料,除錯的時候總不能看著 gdb 印出來的 x, y 座標 debug,所以公司同仁有自幹一套 debug 的工具幫忙把這些資料畫出來;不過呢…這套工具好像在記憶體模型那邊有點問題,資料量大的時候會變超慢;畫圖是直接 call <a href="https://tronche.com/gui/x/xlib/graphics/drawing/" target="_blank">xlib</a>,每次放大縮小都要重畫所有物件,對記憶體的負擔又更嚴重。只要 polygon 數量突破幾萬個的時候,一次的 refresh 就會花上好幾秒。<br />
<br />
前陣子手上暫時沒其他緊急的事情,乾脆就用 Qt Graphics 重寫一個,不準在留言問我為什麼不用 nodejs 寫,MaDer 公司的工作站就沒有 nodejs。<br />
完工後自己試了一陣發現幾十萬個物件的時候放大縮小都超流暢,不愧是 Qt Graphics,雖然程式行數比我預期的多了些,但架構比本來的東西清楚很多。<br />
其實過程中一直參考強者我同學 AZ 大大的 <a href="https://github.com/aitjcize/QCamber" target="_blank">QCamber</a>,覺得 AZ大大實在太過<b>神猛狂強溫</b>,5-6 年前就寫得出這麼複雜的 project,我一直覺得我寫 code 的時候的整體感很不夠,都是在單一 class 裡面塗塗抹抹,小地方會動可是大架構沒辦法在一開始就訂好,後續要修改的成本就非常高。
Anyway 總之它現有個樣子了,我覺得中間碰過一些實作的問題值得記錄一下,預計可能寫個三篇左右吧。<br />
<br />
----
<br />
<br />
首先圖形顯示的部分,使用的是 Qt 的 graphics framework,可以用來繪製大量的 2D 物件,支援選取、縮放等等,我們這裡只是要顯示而已,也不用搞得這麼複雜。<br />
Graphics framework 裡的三個基本元件就是:<br />
<ul>
<li>QGraphicsScene:場景,可以把它想成一塊巨大的畫布,可以在上面自由放上各種 item,Scene 會幫你管理物件的顯示和更新,個人經驗 Scene 負擔到 10 萬個元件左右還很流暢,上到百萬個的時候就會有點頓了(又或者是我把所有 item 都放在 scene 裡面的關係)。</li>
<li>QGraphicsItem:物件,可以想像成畫素描的時候放的那些石膏,在一個場景上擺上東西,Qt 有提供基本的幾種物件:橢圓 QGraphicsEllipseItem、路徑 QGraphicsPathItem、多邊形 QGraphicsPolygonItem、矩形 QGraphicsRectItem跟文字 QGraphicsSimpleTextItem。</li>
<li>QGraphicsView:View 是唯一可以在 Qt 的 MainWindow 畫面上出現的物件,可以把 View 想成一台相機,場景 Scene 是不動的,相機從各種角度自由取景,並把取到的景顯示出來,如果取景的尺寸比畫面還要大,跟其他的物件一樣, View 能自動出現捲軸,也可以接收畫面上的滑鼠、鍵盤事件。</li>
</ul>
只用 Qt 原生的 QGraphicsScene, QGraphicsView, QGraphicsItem 只能組出最基本的顯示工具,變化量非常少,以下就示範一個最基本的設定:<br />
<pre class="notranslate prettyprint">#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QGraphicsScene *scene = new QGraphicsScene;
scene->setSceneRect(0, 0, 400, 400);
scene->addItem(new QGraphicsRectItem(50, 50, 150, 100));
QGraphicsView *view = new QGraphicsView;
view->setScene(scene);
view->show();
return app.exec();
}
</pre>
編譯執行就可以看到這個畫面:
<br />
<div class="separator" style="clear: both; text-align: left;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifsbAWkdBU3P8mqbcIkzEWyRon9SZRypI_PtUZcDzxO4it_jHKMIIRz9EZHS_CJgc5MMhaBJDP_5wOa_qDG1JRyRBycOQXo-BG6X-cqXJHd8UCPxnsBIn6tCkgQuRtFUfb-JefFKYqlAgV/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2019-12-04+22%253A56%253A21.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="432" data-original-width="410" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifsbAWkdBU3P8mqbcIkzEWyRon9SZRypI_PtUZcDzxO4it_jHKMIIRz9EZHS_CJgc5MMhaBJDP_5wOa_qDG1JRyRBycOQXo-BG6X-cqXJHd8UCPxnsBIn6tCkgQuRtFUfb-JefFKYqlAgV/s320/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2019-12-04+22%253A56%253A21.png" width="303" /></a></div>
<br />
反正這個設計只是一開始建來試驗用的,看一下顯示的效果,很快下一篇就會被我們拆掉了。
Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com3tag:blogger.com,1999:blog-2090850666133584855.post-35933190110231575482019-11-16T19:55:00.001+08:002019-11-27T11:35:36.256+08:00把一顆樹寫出來是會有多難<meta property='og:image' content='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_-tTPLoT9S7WeQwucGcmogWca6W2fLKKtUX7_2NDyvur_f4ehgnNfH8wM3zlVxTQ0pgfWZylE_BN1K42z-UtFUKKEjWUpwwXjV0QwBZdHP1tRb_v29yJiJudai-vwnS7aRut3Rdwf-a4j/s320/test.png'/>
故事是這樣子的,之前小弟發下豪語想用 Rust PEG 寫一個 C Parser,然後…就沒有然後了。好啦當然不是,不然就不會有這篇文了。<br />
總之最近經過一陣猛烈的攪動之後,我的 parser 能處理的文法終於接近當年在學校修 compiler 的時候所要求的 B language 了,說來慚愧,當年寫 compiler 作業的時候 parser 只是裡面一個作業,要在 2-3 週裡面寫完的,結果現在搞半天寫不出個毛,果然上班跟上學還是不一樣,在學校可以全心全意投入寫 code ,週末的時候還可以熬個夜把作業寫出來;現在上班白天要改公司的 code ,晚上回家累個半死不想寫 code 只想開卡車(欸。<br />
<br />
本篇講到的程式碼目前還沒推到遠端上,相關的程式碼可以參考:<br />
AST 的資料結構:<a href="https://github.com/yodalee/carbon/blob/master/src/ast/cast.rs" target="_blank">cast</a><br />
型別的資料結構:<a href="https://github.com/yodalee/carbon/blob/master/src/ast/ctype.rs" target="_blank">ctype</a><br />
既然現在可以處理比較複雜的文法了,再來要做什麼?想說就像作業的要求一樣,把我們處理好的 AST 用 graphviz 寫出去,是會有多難?<br />
<br />
整個 dump graphviz 的進入點是一個函式,接收要倒出來的 AST 跟一個 out,out 的型別是 std::io::Write 的 dyn Write,這樣不管你是要寫到 stdout, stderr 還是寫到檔案都能傳進來,介面會是一樣的,函式的實作當然就是直接了當的把該印的東西都寫出去;另外實作一個 dump_node 幫我們把寫出一個 node 給獨立出來,id 會自動不斷累加,讓 node 的編號不會重複。<br />
<pre class="notranslate prettyprint lang-rust">fn dump_graphviz(ast: CastTop, out: &mut dyn Write) {
writeln!(out, "Digraph AST").unwrap();
writeln!(out, "{{").unwrap();
writeln!(out, "label = \"AST_Graph.gv\"").unwrap();
writeln!(out, "node{} [label = \"PROGRAM_NODE\"]", 0).unwrap();
ast.make_node(out, &mut 0);
writeln!(out, "}}").unwrap();
}
fn dump_node(out: &mut dyn Write, id: &mut u32, label: &str) {
*id += 1;
writeln!(out, "node{} [label = \"{}\"]", id, label).unwrap();
}
</pre>
另外我們要實作的是 make_node,這裡很自然的就是先宣告一個 trait,AST 裡面所有的物件都要實作這個 trait ,就都有 make_node 可以用了。<br />
<pre class="notranslate prettyprint lang-rust">trait ToGraphviz {
fn make_node(&self, out: &mut dyn Write, id: &mut u32);
}
impl ToGraphviz for CastTop {
fn make_node(&self, out: &mut dyn Write, id: &mut u32) {
match self {
CastTop::FuncDeclList(v) => {
let cur_id = *id;
for decl in v {
dump_node(out, id, "DECLARATION_NODE FUNCTION_DECL");
decl.make_node(out, id);
}
if *id != cur_id { // new node
writeln!(out, "node{} -> node{} [style = bold]", cur_id, cur_id + 1).unwrap();
}
},
}
}
}
impl ToGraphviz for FuncDecl {
fn make_node(&self, out: &mut dyn Write, id: &mut u32) {
let parent = *id;
dump_node(out, id, &format!("IDENTIFIER_NODE {} NORMAL_ID", "int"));
dump_node(out, id, &format!("IDENTIFIER_NODE {} NORMAL_ID", self.fun_name));
dump_node(out, id, "PARAM_LIST_NODE");
dump_node(out, id, "BLOCK_NODE");
for i in parent..*id {
writeln!(out, "node{} -> node{} [style = {}]",
i, i+1, if i == parent {"bold"} else {"dashed"}).unwrap();
}
}
}
</pre>
* :本來的作業要求連結第一個 child 的必須是實線,其他的用虛線,這裡沿用<br />
這個實作的問題顯而易見,我們的輸出的實作跟資料綁死了,所以每個 node 裡面的實作都是大費周章,而且 code 很醜。<br />
我們要更抽象化一點,其實輸出樹的邏輯是這樣子的:先寫 child 的 node,然後是自己,回傳自己的 id 給 parent,這樣上一層的人才能畫 edge 出來。<br />
我們實作一個 dump_children 的函式,這個函式會用現在的 id 印出現在的 parent,然後把它跟所有傳進來的 children 畫線連起來:<br />
<pre class="notranslate prettyprint lang-rust">fn dump_children(out: &mut dyn Write, id: &mut u32, label: &str, children: &[u32]) -> u32 {
writeln!(out, "node{} [label = \"{}\"]", id, label).unwrap();
let mut prev = *id;
for child in children {
writeln!(out, "node{} -> node{} [style = {}]", prev, child,
if prev == *id { "bold" } else { "dashed" }).unwrap();
prev = *child;
}
*id+=1;
*id-1
}
</pre>
因為 Rust 函式參數沒有預設值也沒有 overload,為了方便我們可以創一個 dump_nochild 的函式,這樣比較方便:<br />
<pre class="notranslate prettyprint lang-rust">fn dump_nochild(out: &mut dyn Write, id: &mut u32, label: &str) -> u32 {
dump_children(out, id, label, &[])
}
</pre>
現在 make_node 的實作都可以用 dump_children 或 dump_nochild 實作,先對自己的 child 們呼叫 make_node,把回傳值(也就是 child 們印完的 root)收集起來再用 dump_children 印出去就行了:<br />
<pre class="notranslate prettyprint lang-rust">impl ToGraphviz for CastTop {
fn make_node(&self, out: &mut dyn Write, id: &mut u32) -> u32 {
match self {
CastTop::FuncDeclList(v) => {
let children : Vec<_> = v.iter().map(|n| n.make_node(out, id)).collect();
dump_children(out, id, "PROGRAM_NODE", &children);
},
}
*id
}
}
impl ToGraphviz for FuncDecl {
fn make_node(&self, out: &mut dyn Write, id: &mut u32) -> u32 {
let children = [
dump_nochild(out, id, "IDENTIFIER_NODE int NORMAL_ID"),
dump_nochild(out, id, &format!("IDENTIFIER_NODE {} NORMAL_ID", self.fun_name)),
dump_nochild(out, id, "PARAM_LIST_NODE"),
dump_nochild(out, id, "BLOCK_NODE")];
dump_children(out, id, "DECLARATION_NODE FUNCTION_DECL", &children)
}
}
</pre>
這樣看起來就好多了,不過我們還能更進一步,仔細觀察上面的 dump_children 的話,就會發現我們還能用 fold 的方式改寫:<br />
<pre class="notranslate prettyprint lang-rust">// print node, and link with all children
fn dump_children(out: &mut dyn Write, id: &mut u32, label: &str, children: &[u32]) -> u32 {
*id+=1;
writeln!(out, "node{} [label = \"{}\"]", id, label).unwrap();
children.iter().fold(*id, |mut prev, child| {
writeln!(out, "node{} -> node{} [style = {}]", prev, child,
if prev == *id { "bold" } else { "dashed" }).unwrap();
prev = *child;
prev});
*id
}
</pre>
老實說,每次我費了這麼大的工夫,把一堆本來<strike>很黃</strike>很暴力的 code 改簡單,變成最後那樣的很純很 Functional 的 code,我都會在內心懷疑個 100 遍,費這麼大功夫是真的有比較快嗎?當然在維護上可能會好一點,但 Rust compiler 能保證抽象化真的是零成本的嗎?這可能是值得好好討論的議題。<br />
<br />
每個函式都要帶著 out 跟 id 走,很不方便,用一個 struct 把它們裝起來:<br />
<pre class="notranslate prettyprint lang-rust">struct DumpGraphviz {
out: Box<dyn Write>,
id: u32
}
</pre>
dump_children 跟 dump_nochild 變成 DumpGraphviz 的實作,介面變成:<br />
<pre class="notranslate prettyprint lang-rust">fn dump_children(&mut self, label: &str, children: &[u32]) -> u32
fn dump_nochild(&mut self, label: &str) -> u32
</pre>
make_node 的介面則是:<br />
<pre class="notranslate prettyprint lang-rust">fn make_node(&self, visit: &mut DumpGraphviz) -> u32
</pre>
整體就變得清爽多了。<br />
天底下沒有新鮮事,其實我就是在實作 visitor pattern,只是還沒把 visitor 整個抽出來讓不同的 visitor 可以在這上面實作。最後輸出的成品長這個樣子:<br />
<div class="separator" style="clear: both; text-align: left;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_-tTPLoT9S7WeQwucGcmogWca6W2fLKKtUX7_2NDyvur_f4ehgnNfH8wM3zlVxTQ0pgfWZylE_BN1K42z-UtFUKKEjWUpwwXjV0QwBZdHP1tRb_v29yJiJudai-vwnS7aRut3Rdwf-a4j/s1600/test.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="936" data-original-width="1600" height="187" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_-tTPLoT9S7WeQwucGcmogWca6W2fLKKtUX7_2NDyvur_f4ehgnNfH8wM3zlVxTQ0pgfWZylE_BN1K42z-UtFUKKEjWUpwwXjV0QwBZdHP1tRb_v29yJiJudai-vwnS7aRut3Rdwf-a4j/s320/test.png" width="320" /></a></div>
<br />
我有個小小的體悟,就是寫程式不要妄想一步登天,除非如強者我同學 AZ 大大那樣一眼就把超大程式的架構都畫出來,而且實作起來都不會亂掉。<br />
我上一次的實作就是衝太快,翻著 C standard 想要一開始就照著 C standard 實作,然後文法寫得亂七八糟反而連簡單的文法都會大噴射無法處理;與其如此,不如先支援基本的功能,等 parser 跟文法處理都完善之後再慢慢把其他功能加上去。<br />
我覺得用蓋房子比喻的話,寫大程式要像西敏寺那樣的大教堂一樣,先從一個功能完整的小教堂開始,然後把小部分拆掉蓋個更大更豪華的(有看過一個動畫片在演示這個過程的,只不過沒有公開版);如果一次就想蓋個超大的教堂,最後可能弄成一團廢墟,連禮拜的功能都沒有。<br />
<br />Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-38991416038827110552019-11-04T23:31:00.001+08:002019-11-04T23:31:16.782+08:00從 Coscup 小談 Rust這篇其實有點拖稿,畢竟 COSCUP 都是幾個月前的事了;這次在 COSCUP 投稿了 Rust 議程軌,覺得可以來說說對 Rust 的一點感想。Rust 從問世、正式發佈到現在也差不多要 7 年,感覺近年來有愈來愈紅的趨勢,一種社群上面看一看發現大家都用過 Rust 的感覺。<br />
<br />
今年的 COSCUP 專門開了一個 Rust 議程軌,而且感覺議程的內容正在提升,不再是一堆語言介紹,有更多的是在介紹用 Rust 實作的資料庫、web assembly 、類神經網路的應用,可以預見 Rust 正在走出推廣階段,前往實際應用的領域。<br />
不過我們還是要回來問,Rust 在哪裡會有<十倍生產力>?也就是在哪裡可以把東西做得比其他語言十倍好,像是要推人工智慧大家就會推 Python;要寫高效能的網路可能會用 golang,有哪個領域是非用 Rust 不可的嗎?現在有些風聲是區塊鏈的合約和交易語言,但我對這塊應用的大小有點存疑。<br />
<br />
Rust 天生尷尬在它的定位上,它的目標是一個安全高效的系統程式語言,它也的確有潛力做到這點,但整體看來 Rust 可能是幾大系統程式語言裡數一數二複雜的,可能只輸給 C++,配上最新加上去的 Async 可能差不多就比肩了(欸。<br />
確實 Rust 從源頭來看,受到大量函數式語言和語法的啟發,語法上看得出核心來自一個優異的語言團隊並吸收了各類語言的優點;編譯時進行的所有權確認和以 mod 為編譯單位,雖然讓 Rust 編譯慢得像烏龜,卻也大量消除程式在執行時出錯的機會,或者因為設計師<忘記>而導致的問題。<br />
Rust 不可能是一款早期的語言,它浪費太多運算資源在編譯檢查,在 C 語言發跡的年代不會浪費資源去做那些檢查,換來的就是 Rust 編譯器數一數二的 GY 程度,這個不行那個也不行,搞得寫 code 的人跟編譯器都很累……。<br />
<br />
我認為 Rust 要走的會是一條很艱難的道路,Rust 內建的複雜性天生就拒絕了一些簡單的應用,用 Rust 寫起來太過繁瑣了,動態語言能搞定的網路服務開發速度是第一,程式設計師上手的速度還有開發的速度來看,沒理由不用動態語言;而一些偏底層的應用,特別是對從 C/C++ 來的人來說,Rust 根本就不可理喻,明明我用 C 系列一下就可以搞定的,誰跟你在那邊 4 種 String 還有一堆 Option 要處理?一眼看穿的程式實在用不上 Rust,有人覺得 Rust 可以在嵌入式系統上挑戰 C,我看再過 100 年都不太可能。<br />
Rust 的優勢,要來到所謂的大型系統程式才會出現,透過編譯器的強制,把一些難以檢測到的記憶體問題給挑出來,當然用 C++20 的一些特性可以做到一樣的效果,但沒有編譯的強制只靠設計師所受的教育,在大型系統下畢竟不是一個妥當的做法,畢竟設計師也是人,不可能不犯錯,或者偷懶或者忘記,一不小心就引入 C++ 的舊語法 -- 那些為了向後相容絕對不會移除的部分。<br />
<br />
但問題就在於:大型系統幾不太可能整個重寫,更別提底層所依賴的都是經過千錘百鍊的 C/C++ 函式庫,像 Mozilla 那樣決定把瀏覽器核心整個抽換掉真的是<strike>神經</strike>勇敢,市面上的大公司哪幾家做過一樣的事?<br />
可以預期 Rust 幾年之內,都會是用滲透的方式慢慢進到各大公司的系統當中,也許是一個新實作的子系統或是重寫某些小部分,用 FFI binding 的方式和既有的系統銜接,但要成為主流我看還要努力一段時間才行。<br />
<br />
其實我是覺得語言比語言氣死人,不過 Rust 對 go 一直是一個大家很有興趣的話題(雖然說兩個根本是完全不同的東西),我個人滿推薦 LoWeiHang 翻譯的<a href="https://gist.github.com/weihanglo/3dc1af4b0c15cb9ec600f28a7b06ad2f" target="_blank">這篇文章</a>。Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-43908772828443902042019-09-08T22:41:00.001+08:002019-09-08T22:46:03.574+08:00從 C 呼叫 Lua 函式故事是這樣子的,小弟在公司裡面,主要是負責維護一個<strike>沒人在用的</strike>產品,遠觀來說這個產品滿複雜的,內建兩種不同的演算法實作,為的是要應對不同的狀況,有些狀況用第一種演算法比較快,有些用第二種。<br />
那故事是這樣子的,我們的程式裡面有一個函式會在每筆資料結束之後,用上筆資料的結果來判斷下一次要選哪個演算法,問題是這個函式目前是直接寫在整個引擎的 C code 裡面,於是如果想要改變一下判斷的標準……sorry 重新 build,雖然公司弄了套分散編譯可以編很快但還是要幾分鐘。<br />
上星期自己試了一下,成功把 Lua 編到公司的 code 裡面,就能把判斷邏輯寫在 Lua script 裡面,要改判斷標準只要改 Lua 就可以了;我一般聽到會這樣用的是遊戲公司,因為遊戲一樣涉及大量的邏輯判斷,例如血要扣多少之類的,而遊戲又會大量的去變動這些參數,使得彈性變得非常重要,總不能要改參數就把整個遊戲引擎全部重建構一次……說是這麼說我也不曾證實哪家公司真的這麼做就是,如果我的讀者真的是這樣搞的麻煩留個言讓我知道一下。<br />
<br />
總之自己試過之後其實非常簡單,難怪大家都說 Lua 最厲害的就是嵌入到 C 程式當 Sup 角 ,我主要是參考<a href="https://www.cs.usfca.edu/~galles/cs420/lecture/LuaLectures/LuaAndC.html" target="_blank">這個網頁</a>(看來是舊金山大學 CS 的課程頁面);下載 Lua 就從<a href="https://www.lua.org/download.html" target="_blank">官網下載</a>即可,哪一版應該是無所謂,或者是 Linux 的話安裝開發套件也 OK。<br />
不愧是以輕量著稱的腳本語言,連 Makefile 看起來都是手寫的,直接下 Make 讓它編譯完成,雖然說我在這步被環境變數 TARGET_VAR 卡了很久,makefile 不知道為什麼自己把它加到編譯參數裡了。<br />
再來做下面幾件事就能呼叫 Lua 函式了:<br />
<h4>
1. 引入 Lua 的標頭檔:</h4>
<pre class="notranslate prettyprint lang-c">#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
</pre>
然後在編譯的時候記得給一下 lua 標頭檔的位置,以及在連結的時候 -llua。<br />
<h4>
2. 生成 Lua state:</h4>
Lua 的 state 包含核心的函式,後面所有的函式都會需要這個 state;在 open 和 close 中間就能呼叫 Lua 函式了:<br />
<pre class="notranslate prettyprint lang-c">lua_State* L = luaL_newstate();
luaL_openlibs(L);
// write lua_call ... code here
lua_close(L);
</pre>
<h4>
3. 載入檔案,呼叫函式:</h4>
使用 luaL_dofile 打開 lua 檔案,等於是呼叫 lua 直譯器執行這個檔案,如果有直接執行的東西這時候就會有效果,像是 print("hello world") ,我們這裡只是定義變數 "add" 對應到相加的函式。<br />
要呼叫 lua 函式,我們以函式 add 為例:<br />
<pre class="notranslate prettyprint lang-lua">-- add.lua add function
add = function(a, b)
return 42
end
</pre>
那麼在 C 裡面就是:<br />
<pre class="notranslate prettyprint lang-c">luaL_dofile(L, "add.lua");
lua_getglobal(L, "add");
lua_pushnumber(L, a);
lua_pushnumber(L, b);
lua_call(L, 2, 1); // 2 parameter, 1 return value
int sum = (int)lua_tointeger(L, -1);
lua_pop(L, 1)
</pre>
簡單來說就是先用 lua_getglobal 拿到存在 global 裡面的函式 add(在 luaL_dofile 的時候建立的);把參數推到堆疊上;執行 lua_call,指定兩個參數跟一個回傳值,取出回傳值,把回傳值彈出堆疊。<br />
如果函式有多個回傳值的話,會依序放在堆疊的 -1, -2 … 上;lua_pop 也要彈出更多值;Lua 的有一系列的函式來把東西推到堆疊上/彈出堆疊,簡單的應用,通常就是 lua_pushinteger/lua_tointeger, lua_pushnumber/lua_tonumber 來推/彈整數跟浮點數到堆疊上,詳情請<a href="https://www.lua.org/manual/5.3/" target="_blank">參考文件</a>。<br />
<br />
這裡只記錄最基礎的應用,實際上應該還有更複雜的應用,例如我要做決策的參數如果很多的時候該怎麼辦?或者其實我想要直接推一個 struct 進到 lua 是可以的嗎?<br />
這兩個問題我目前都沒有答案,還有待研究,目前只知道 luajit 這個看起來好像停擺的專案有提供類似的東西,可以準備好 C 的 struct 來吃。<br />
如果有大大們有答案的話麻煩教一下小弟,小弟現在很需要。Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0tag:blogger.com,1999:blog-2090850666133584855.post-80365703492704516432019-09-04T23:23:00.000+08:002019-09-04T23:23:55.490+08:00Rust 裡面那些 String 們故事是這樣子的,最近把小弟自幹的編譯器加上 rust 的 llvm wrapper llvm-sys,經過一陣猛烈的攪動之後,自幹的編譯器終於可以 dump LLVM IR 了,雖然只會輸出一個空殼子…但有第一步總是好的。<br />
不過小弟在綁定的時候遇到一個大問題,也就是 Rust 裡面的 String,到底怎麼會有這麼多種,因為寫的時候一直沒搞清楚,然後就會被編譯器噴上一臉的錯誤,覺得痛苦,於是決定來打篇整理文。<br />
簡單來說,Rust 的 std 有四種 String,每個 String 都有動態記憶體模式跟沒有 size 資訊(不是 Sized)的靜態模式,他們是:<br />
<div class="hl notranslate">
std::string::String <-> std::str<br />
std::ffi:OsString <-> std::ffi::OsStr<br />
std::path::PathBuf <-> std::path::Path<br />
std::ffi::CString <-> std::ffi::CStr</div>
還有一個比較少用,只能表示 ascii 128 字元組成的字串的 std::ascii::asciiExt,這裡就不介紹了。<br />
<br />
一般的程式語言在數字型態通常都很固定,Rust 就很明確的分為 i8, i16, i32, i64 …,就偏偏字串是個大坑,因為從 ASCII 到 unicode,字串實在有太多分岐,儘管有 unicode 也不是到處適用。Rust 從設計上一開始就直接採用 utf-8 作為設計標準,原生的 String/str 就是 utf 8 字串。<br />
可是呢,並不是所有作業系統都玩 utf8 這套,因此 Rust 有另一個使用 wtf8 的 OsString,wtf8 跟 utf8 的差異在於 wtf8 算是<格式比較差>的 utf8,會出現一些 utf8 不允許的位元組,偏偏規格沒有要求一定要完美格式,造成 windows 或 javascript 有時會出現這種格式不良的 wtf8 字串,因此 OsString ,跟專門用來表示路徑的 PathBuf 就是使用 wtf8。<br />
有關 wtf8 請參考:<a href="https://simonsapin.github.io/wtf-8/">https://simonsapin.github.io/wtf-8/</a><br />
<br />
上面的字串都是在型態中記錄字串長度,結尾不會有 \0 字元,CString 則是最傳統的 null-terminated 字串,在呼叫 C 函式的時候,一定要用 CString 傳遞才行。<br />
順帶一提,一般寫在 code 裡面的 let hello = "hello world" 的型態是 &'static str:生命週期為 static 的靜態字串。<br />
<br />
知道了以上幾個區別之後,就來看看要怎麼使用它們:<br />
String 最簡單,裡面一定要是 utf8,產生就是從 static str 產生,或者是 new 之後慢慢 push 進去:<br />
<pre class="notranslate prettyprint lang-rust">let hello : String = String::from("hello");
let mut world : String = String::new();
world.push_str("world");
world.push('!');</pre>
OsString 是類似的,但只能從 String 轉過來(注意 String 的所有權會轉給 OsString),或者一樣 new 之後 push String 進去:<br />
<pre class="notranslate prettyprint lang-rust">use std::ffi::{OsString, OsStr};
let oshello : OsString = OsString::from(hello);
let mut world : OsString = OsString::new();
world.push("world!");</pre>
PathBuf 其實就想成 OsString 就好,兩者也可以互相用 from 轉換:<br />
<pre class="notranslate prettyprint lang-rust">use std::path::{PathBuf, Path};
let p1 : PathBuf = PathBuf::from(oshello)
let mut p2 = PathBuf::new();
p2.push("/dev");</pre>
上面說了,OsString 跟 PathBuf 用的是 wtf8,是 utf8 的超集,因此一般只能單向從 String 到 OsString,反向是不行的,呼叫 OsString::into_string() 得到的是 Result<String, OsString>,也就是有可能會轉失敗;或者就是用 into_lossy_string 把編碼不完整的地方變成 U+FFFD,utf8 的 replacement character。<br />
PathBuf 則是沒有 into_string 可以用,只能先轉換成 OsString 再轉過去,我也不知道為什麼 core team 要這樣設計。<br />
<br />
剩下的就是函式了,很有趣的是 String, OsString, PathBuf 都是動態容器,操作內容都要轉換到 str, OsStr, Path 上面去:<br />
str 有操作字串用的 split_whitespace, starts_with 等等<br />
OsStr 沒有任何特殊的函式XD。<br />
Path 有很多對路徑的操作:is_absolute, parent, with_extension 等等,很多函式操作後都會得到 Path 或是 OsStr 讓你做接下來的操作。<br />
<br />
CString 比較棘手一點,它要在 new 的時候代入 Vec<u8> (或者有實作 Into<Vec<u8>> 的型態)來建立 CString,new 會自動在後面加上 \0 ,因此這個 Vec 裡面不應該有 \0。<br />
其實我覺得把 CString 想得 Vec<u8> 的另一種型態就好了,它本身也提供 into_bytes, as_bytes 等函式轉換成 Vec<u8> 的型態。<br />
如果要從 String 跟 OsString 轉換過來的話,String 要用 as_bytes() 轉成 Vec<u8>,OsString 因為 unix 跟 windows 會有不同的 OsString 實作,不一定都能轉成 Vec<u8>,在 unix 要引入 std::os::unix::ffi::OsStrExt 就可以將 OsString 用 as_bytes() 轉成 Vec<u8>;Windows 則建議轉成 String 再轉成 bytes ,請參考這個<a href="https://stackoverflow.com/questions/38948669/whats-the-most-direct-way-to-convert-a-path-to-a-c-char" target="_blank">網址</a>。<br />
<br />
用上了 CString,最重要的就是要交給外部的 C 函式去用,要用 as_ptr() 取出字串部分的 pointer,得到的就是 * u8 了,有必要的話再加上 as *const i8 轉型一下。<br />
例如我要呼叫這個函式:<br />
<div class="hl notranslate">
<a href="https://llvm.org/doxygen/group__LLVMCCoreModule.html" target="_blank">LLVMPrintModuleToFile </a>(LLVMModuleRef M, const char *Filename, char **ErrorMessage)</div>
這個函式,我的檔案名稱是一個 OsString:<br />
<pre class="notranslate prettyprint lang-rust">use std::ffi::{CString, CStr};
use std::os::unix::ffi::OsStrExt;
llvm::core::LLVMPrintModuleToFile(
self.module,
path.as_bytes().as_ptr() as *const i8,
ptr::null_mut());</pre>
看了這麼多,簡單整理一下大概是這樣:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEO9oYVcIlAggzcTKeA5jjMjLox5vqy3yuUFEg-5RDO6Q-Ij09-hxC1PZHPec41UkeCnw9DJypMnwz1YXb5-6XqS32hslDmK5f_J7ZdhKs2Wz2CS5tL9SLH3SiZc8i89NnIPk2vGPquSz2/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2019-09-04+23%253A05%253A40.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="433" data-original-width="833" height="332" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEO9oYVcIlAggzcTKeA5jjMjLox5vqy3yuUFEg-5RDO6Q-Ij09-hxC1PZHPec41UkeCnw9DJypMnwz1YXb5-6XqS32hslDmK5f_J7ZdhKs2Wz2CS5tL9SLH3SiZc8i89NnIPk2vGPquSz2/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7+2019-09-04+23%253A05%253A40.png" width="640" /></a></div>
<br />
老實說每次只要在 Rust 裡面弄到 Path 都會弄到懷疑人生……Lee You Tanghttp://www.blogger.com/profile/15039846730113669754noreply@blogger.com0