Artillery game

炮術遊戲(Artillery)是早期回合制策略電子遊戲的代名詞,坦克互相射擊,考慮彈道計算、地形、彈藥和風向等問題。

這類型的遊戲最早在1970年代中以demo遊戲出現,名叫Artillery(抱歉太舊的關係,我在維基也找不到相片),後來隨著顯卡的提升,到1980年代在Apple II中推出過Super Artillery和Artillery Simulator。

img

後來再到IBM電腦年代,推出過名叫Scorched Earth的遊戲。這款遊戲除了之前炮彈互射的遊戲功能外,當炮彈打到地後,也會留下一個洞,這就需要電腦的效能支援。

img

按著在1995年由Team17推出的Worms(蟲蟲大戰),就真正是經典遊戲。遊戲同樣是探用回合制,但今次蟲蟲是可以行走的,而且可以好似蜘蛛俠一樣盪來盪去(我自己玩過的版本就是,不知道1995年的初版是否可以),而且有不同的武器可以選擇,有些是威力強大到會留下大洞的炮彈,有些是手槍和地雷等等。後來這遊戲一直都有推出不同的版本,到今天你也可以在各大遊戲平台買得到這款遊戲,玩法也差不多。(不過我記得我玩過的版本並非回合制的,而是畫面分半一同操作的,兩個人在電腦面前按著同一個鍵盤,互相找出對方)

Worms (1995) [MS-DOS] - YouTube

順帶一提,千禧年初,3A大作已經十分流行,當年網路剛興起,網站流行用flash制作動畫效果,同時也興起一堆體積不大的小遊戲,例如比加超打排球,小朋友落樓梯,小朋友齊打架等等,而其中一款中外同樣經典的,就是Fleabag Vs. Mutt,我們一般會叫它貓狗大戰。玩法就跟80年的的Artillery Simulator一模一樣,都是採用回合制,設定投擲力度和角度,配合環境風速,互相擲罐子或骨頭。遊戲雖然簡單,但因遊戲體積小,容易得到容易上手,當年是跟朋友殺時間的好幫手。(我不也肯定下圖是原版flash遊戲,網路上也找不到原版的flash檔)

貓狗大戰- 遊戲下載| TapTap

今章要制作的,是類似上面的Artillery Simulator和貓狗大戰的回合制遊戲。

0. 本章重點

  1. 向量(vector)(求大小與方向)

  2. 加速度與拋物線

  3. 用歐拉方法求積分解

  4. 函數(Function)應用複習

1. 加速度與拋物線

1.1 自由落體

傳說1590年伽利略曾在義大利比薩斜塔上做自由落體實驗,將兩個重量不同的球體從相同的高度同時扔下,結果兩個鉛球同時落地,伽利略在比薩斜塔做自由落體實驗的故事,記載在他的學生維維亞尼在1654年寫的《伽利略生平的歷史故事》(1717年出版)一書中,但伽利略、比薩大學和同時代的其他人都沒有關於這次實驗的的記載。對於伽利略是否在比薩斜塔做過自由落體實驗,歷史上一直存在著支持和反對兩種不同的看法。

1971年,阿波羅15號太空人在月球上同時丟下獵鷹羽毛與鐵鎚,證明伽利略理論正確。

img

 

在真空(無空氣阻力)的狀況下,一個物體自由落體的距離,受著重力影響,落下距離與時間的平方成正比。舉例說,在下圖是用攝影機拍攝跨度為半秒的相片(半秒20幀),在首0.05秒落下的距離為1個單位(約12mm),在0.1秒時,其落下距離為4個單位,在0.15秒時距離為9個單位,如此類推。

img

1.2 拋物運動

由於受到重力的影響,物體在被拋出後,垂直(向下)的速度會不停加速,而水平的速度則不受影響,型成的曲線就叫拋物線(Parabola)。拋物線(Parabola)在數學上是一條二次方曲線。

img

等等等,不要走,我知道你快要頭腦爆炸了!!!!!!

這裡不想帶出更多的數學和物理,令事情更複雜,你唯一需要知道的是:

  1. 重力影響會令落下距離與時間的平方成正比,簡言之,落下的速度與時間成正比,地球自由落體加速度大約是10m/s2,即在物體自由落體時,1秒後的速度大約是10m/s,2秒後的速度大約是20m/s,3秒後的速度大約是30m/s,如此類推。

  2. 計算拋物線時,水平和垂直的距離和速度是可以分開計算的,互相獨立; imgimg

2. 建立兩個玩家

image-20221011104813227

 

setup()中,加入這兩句來初始化兩個玩家一開始的高度。由於想加一點遊戲性,盡量兩個玩家是一高一低。

3. 繪畫控制的投射速度線

image-20221011112919681

 

在宣告區,開設一個變數叫Round紀錄現在這個回合是player1還是player2(記得Round要大寫, round是processing.py原本的指令,是用來使四捨五入的)。另外開設兩個向量變數,用來紀錄炮彈的位置和速度。

 

同樣地,在setup()區中,初始化這三個數的值,方便之後可以一鍵重啟這個遊戲。ballPos的值設定在player1相同的位置。而ballVec的初始值則設成(5,0)

4. 控制投射速度線

control line

setup()draw()加入第三個內置的函數叫做keyPressed()。當Round == 1,即現在的玩家時player1時,按下ws鍵,則是控制速度向量的大小,利用PVector內置的乘法,就可以將向量乘大或者乘小;當按下a或者d時鍵,則控制其方向的變化,同樣地,PVector內置了功能,可以將向量旋轉,不用自己做數學運算。值得一提是,processing.py所有的角度,預設都是radians(弧度)的,要用指令radians()將輸入的角度轉換成弧度。有關甚麼是弧度可以參考這裡,而有關甚麼是向量乘法可以參考這裡,最後向量旋轉則較為複雜,可以參考這裡

img
processing的旋轉角度跟數學不同,是順時針而非逆時針的

5. 嘗試直線發炮

control line 2

 

在宣告區加多一個變數名為trigger,用來在確認航道時發射炮彈的。記得在setup()區中也需要初始化一次,方便之後restart遊戲。

 

draw()中,加入,如果triggerTrue的話(下面的keyPressed()函數控制),即準備好要發射;跟之前彈珠一樣,每次draw()更新時,都將ballVec這個向量,累加到ballPos這個向量中,就會見到炮彈向著直線發射。

 

在最下的keyPressed()函數中,加多兩個鍵盤按鍵,一個是空白鍵,用來發射;另一個跟之前一樣,用r鍵用來重設遊戲。

 

6. 加入重力(加速度)

control line 3

 

在宣告區中,加入球的加速度,今次不用在setup()中再初始化,因加速度是保持不變的。由於processing.py的y軸是向下的,所以不需要像物理一樣,將加速度設成負數。

 

draw()中,將原本繪畫速度箭頭的位置,加入發射後就會消失,否則一改變速度向量,你就會見到速度箭頭會跟著向下,會有點奇怪。

同樣地,在keyPressed()函數中,原本的if Round == 1:,要額外加上if Round == 1 and not trigger:,否則在開炮後,也能"遙控"炮彈。

 

回到draw()中,要做到拋物線效果,其實很簡單,也很"物理",加速度的意思是每一秒的速度也在累加,而球的位置則是初始位置再累加速度,所以只要在每次draw()更新時,將向量ballAccel加上向量ballVec,之後再將向量ballVec,加上向量ballPos,就可以模擬到拋線的效果。

 

7. 加入風速

這類型的炮彈遊戲,為增加可玩性和模糊真實環境,會加入風速的選項。

control line 4

在最上方的宣告區中,宣告一個global的變數叫windAccel

 

setup()區中,初始化這個值。

 

之後在draw()區中,在更新球的位置時,除了y軸的地心加速度,額外加入水平的x軸加速度。

之後就可以先試一試是否成功才進行下一步。將炮彈發射方向指向天來發射,就可以很清楚地看得到,如果風速沒有成功,炮彈會直上直下,但如果風速成功的話,就會被次向左邊或者右邊。

8. 判斷是否落地

 

跟之前一樣,在setup()draw()之後,額外開一個函數(function),方便我們將功能相似但重覆的功能組合在一起。

今次函數有3個輸入,分別為_ballPos, _topLeftCorner_bottomRightCorner,顧名思意,是球的座標,左上角的座標和右下角的座標。內容也很簡單,如果球落入這個範圍就傳回True,否則就傳回False

 

之後在draw()的最後,加入這一段:開兩個變數,如果打中player1的地下範圍或打中player2的地下範圍,則print("something"),用來測試一下程式是否正確。

9. 判斷是否打中P1或P2

image-20221014105451376image-20221014105028895

 

在程式的最後面,除了之前的setup()draw()isHitGround()之後,額外再加一個函數名為isHitPlayer()isHitGround()isHitPlayer()其實十分相似,但前者第二和第三個輸入是方型的兩個對角,而後者由於player一開始繪畫時是先定義方型的中心,再定義方型大小的,故分開兩個函數方便運用。

我特意放大了兩部坦克的大小,方便測試一下是否正確,由於一開始球的初始位置就是在player1的中心點,所以程式一運行,就會不停彈出P1 hit,之後如常調試和發射(你可能要測試好幾次才能成功),炮彈先是擊中player2, 之後再打到地面。

10. 打中地面後變成對方回合

image-20221017100923259

接著,就可以更新一下當炮彈打到地下後該發生甚麼事。

 

當打中地面後,之前我們簡單測試時就只print一此文字出來測試效果,現在要告訴程式該發生甚麼事。打中地面後,如果現在是player1的回合,那麼就要變成player2的回合,反之亦然。球的初始位置也是,如果是player1的回合, 初始位置就是player1的位置, 如果是player2的回合就是player2的位置。接著重置球的初速ballVectrigger變回False表示未發射,最後重置每回合的風速。(記得在draw()的設定這些變數做global變數)

 

另一個要更新的地方是keyPressed(),之前我們只設定了player1是用W, A, S, D四個鍵,而現在要設定player2用鍵盤的方向鍵。比較特別的是,方向鍵為特別鍵,如果按下了,之前的變數key就會變成特別變數CODED,這些CODED特別鍵除了方向鍵外,也包括了ALTCONTROLSHIFT這些不在ASCII編碼1的鍵盤按鍵。

而要讀取這些不在ASCII編碼的鍵盤接鍵,就要用另一個特別變數叫keyCode,當按下這些接鍵時,key會變成CODED,而keyCode也會相應地變成這些特別按鍵。

11. 設定擊中對方後加分和進入下一回合

image-20221017105320206

開始寫之前,你會發現,每次球擊中對方或者球落地時,其實來來去去重設都是那幾句,不停地重覆,但每次都有少許不同,對,這時候就非常適合用函數了。開一個函數叫reset(),將原本擊中地下時重設的幾句都複製進去,唯一不同的是,今次函數有一個輸入,判斷現在回合不同作出不同反應。

 

現在setup()就簡潔很多了,當初好幾句設定初始的球位置、球速和風速的程式碼只需要化簡成一句就可以了。(記得是要設定為reset(2),告訴程式現在是player2的回合,它才會在下一步設定為player1的回合)。而且你會發覺,當初一大堆global的變數,由於現在都不在setup()中設定變更,所以可以刪除,只剩4個。(同樣情況,在draw()中也是,只需要剩下這4個global變數需要變更)

 

開設兩個變數score1score2,在宣告區宣告和在setup()中設定初始值。接著在draw()中,將之前球擊到在地時的幾句化簡成reset(Round),打中地後現在是哪個回合就重設哪個回合。

而之前打中對方時,只是print是否打中來測試,現在可以補回了,如果打中player1而現在又是player2的回合時,就為player2加分,否則,打中player2而且是player1的回合,就為player1加分。

最後將分數顯示在畫面上方。我在測試時發現很難即時判斷現在回合是player1還是player2,所以在分數的顏色上加了效果,會用紅色和藍色代表回合。

12. 考考你

  1. 當你測試時,你會發現有一個問題,就是當炮彈打不中地,又打不中對方,而飛出了畫面左邊或右邊時,是不會進入下一回合的(超出畫面上方不進入下一回合是刻意為之的)。我們修改一下遊戲,原遊戲設計並沒有這個設定,當球打中左邊或右邊畫面時,會反彈轉變方向,就像player1和player2身後有一面牆一樣。

  2. 原遊戲的風速有一個箭頭和文字顯示,方便用家跟據風速決定修正。請你幫遊戲加入這個風速箭頭,並將原來風速random的上下限加大一點,令遊戲更受風速的影響。

    img


1 ASCII(發音: /ˈæski/ ASS-kee,American Standard Code for Information Interchange,美國標準資訊交換碼)是基於拉丁字母的一套電腦編碼系統。