ckan で Kerbal Space Program プログラミング環境を setup する

## TL; DR
Buy [Kerbal Space Program](https://kerbalspaceprogram.com/en/) now (available on [Steam](http://store.steampowered.com/app/220200/Kerbal_Space_Program)) 
Download [ckan.exe](https://github.com/KSP-CKAN/CKAN/releases) and execute
Install any of [kRPC: Remote Procedure Call Server](http://krpc.github.io/krpc/index.html) / [kOS: Scriptable Autopilot System](https://ksp-kos.github.io/KOS/) / [MechJeb](http://wiki.mechjeb.com/index.php?title=Manual)
gl hf!

## Kerbal Space Program (KSP) とは

<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">Kerbal Space Program 1.3: Away with Words - 予告編 <a href="https://t.co/sSGnQTSXzV">https://t.co/sSGnQTSXzV</a></p>&mdash; haeena (@haeena) <a href="https://twitter.com/haeena/status/940210539342393345?ref_src=twsrc%5Etfw">December 11, 2017</a></blockquote>

自分だけの宇宙開発プログラム立ち上げよう！
謎の緑の宇宙人[^1]とミニチュアの太陽系[^2]にリアルな物理シミュレーション[^3] を載せて！ 

KSPとは一言でいうとそんなゲームです。

このゲームの面白いところは、物理シミュレーションが容赦なくあなたの打ち上げるロケットを襲うあたりにあり、当初はロケットを惑星の周回軌道に乗ることすら難しいのですが、一度わかってしまえば月旅行や宇宙ステーション建造、外惑星への探査旅行すら可能[^4]という自由度にあります。

開発元はメキシコに拠点を置くSquadという会社ですが、先日リリースされた v1.3 にて日本語対応を果たし、横文字の苦手なお年寄りから小さいお子様まで実際安心になりました。

Vanilla も大変プレイアブルなのですが、KSPは皆さんの大好きな mod の開発も大変盛んで、UI変更・空力シミュレーションモデルの変更・パーツ追加・宇宙基地・変形・合体・ワープです！、と各種取りそろえられておりますが、中でも今回は **プログラムを書くことで宇宙船を操縦する**[^5] modを導入して、KSPのシミュレータを遊び倒す方法をご紹介したいと思います。

未購入の方は、とりあえず買ってから考えましょう。
なぁに、最悪でも積んでるゲームが一つ増えるだけですって
-> Buy [Kerbal Space Program](https://kerbalspaceprogram.com/en/) now (available on [Steam](http://store.steampowered.com/app/220200/Kerbal_Space_Program)) 


## ckan の導入 / mod の install

[ckan](https://github.com/KSP-CKAN/CKAN) は 有志によってメンテされている、KSPのmodマネージャです。
KSPのmod配布は [CurseForge](https://www.curseforge.com/kerbal/ksp-mods) / [Spacedock](https://spacedock.info/kerbal-space-program) / [KSP Forum](https://forum.kerbalspaceprogram.com/index.php?/forum/34-add-on-releases/) / mod作者のgithub 等の複数サイトで行われており[^6]、希望のmodを一通りそろえて、定期的に最新のmodに更新していくのは一苦労です。
KSP本体も比較的ちょいちょいバージョンが上がっており、古いmodは容赦なく動かなくなる傾向にあります。

ckan は各種modの配布URLや依存関係などの[メタデータ](https://github.com/KSP-CKAN/CKAN/blob/master/Spec.md)をメンテナンス・配布することで、それらmodの導入・更新を飛躍的に容易にしてくれるすばらしいツールです。
ckan は .NET Framework 上にC#で書かれていますが、Monoでも動く[^7]とのこと。


<img src="https://qiita-image-store.s3.amazonaws.com/0/221366/d464e4c3-489b-b9d4-e71b-2850c7daea98.png" />

ckan を起動すると上記のようなGUIからmodの一覧を選択できます。
KSPがinstall済みならKSPのパスは自動で検知してくれるはずですが、環境によっては手動で設定するようdialogが出るかもしれません。
虫眼鏡のアイコンを `Filter(Compatible)`に設定すると、現在installされているKSPのバージョンと互換なmodのみがリスト表示されます。リストの中から導入したいmodのcheckboxにcheckを入れ、`Apply Changes`ボタンで download & install です。

どんなmodがあるのか一覧を眺めて、名前の気に入ったmodのHomepageに飛ぶだけでも楽しいものです。
人気のあるmodであれば、HomepageからYouTubeの紹介動画なんかも見られるかもしれませんね。


さて、プログラマブルなmodの紹介に移ります。

## [MechJeb](http://wiki.mechjeb.com/index.php?title=Manual) 
[Source Code (github)](https://github.com/MuMech/MechJeb2) / [Document] (http://wiki.mechjeb.com/index.php?title=Manual) / [Forum](https://forum.kerbalspaceprogram.com/index.php?/topic/154834-130-anatid-robotics-mumech-mechjeb-autopilot-261-27-may-2017/)
難易度: かんたん / 言語: Visual Programming / Editor: GUI

こいつは使い方は簡単です。Easy modeです。

一つだけ注意することがあるとすれば、VAB/SPH(宇宙船組み立て画面)で`Command and Control`カテゴリからMechJebのパーツを選んで、宇宙船にくっつけてあげる必要があります。

MechJebのついているロケットを打ち上げようとすると、Lunchpad(打ち上げ画面)にMechJebのメニューが表示されますので、その中の `Scripting Module (Beta)` を選んであげると、スクリプトエディタが現れます。
あとはちょいちょいと`Add Action`で動作を指定してあげて、`START`を押したら打ち上げるだけです。

![mechjeb3.png](https://qiita-image-store.s3.amazonaws.com/0/221366/02280ac0-a511-f6cb-1360-970267da9d3c.png)

画像が見にくいですが、今回は下記を指定しています。
Kerbalから打ち上げて、Munの高度200kmの周回軌道に入ってくれるはずです。

```
ASCENT GUIDANCE (100km)
Target Celestial Body: Mun
Hohmann Transfer to Target: Schedule the burn in next transfer window
Execute next node
Fine tune closest approach to target: 200,000m
Warp to: SOI transition Lead time: 0s
Execute next node
Change Apoapsis: New apoapsis: 200km schedule the burn at the next peripsis
Execute next node
```


それだけです。
打ち上げ中の角度調整やステージ切り離し、トランスファウィンドウの計算はすべてMechJebがやってくれます。
ね、簡単でしょう？[^8]


## [kOS](https://ksp-kos.github.io/KOS/)
[Source Code (github)](https://github.com/KSP-KOS/KOS) / [Document](https://ksp-kos.github.io/KOS/) / [Forum](https://forum.kerbalspaceprogram.com/index.php?/topic/165628-13-kos-v1130-kos-scriptable-autopilot-system/)
難易度: ほどほど / 言語: kerboscript (独自) / Editor: In Game (or telnet)

このmodもVAB/SPHで宇宙船にパーツをつけてあげる必要があります。
`Command and Control` カテゴリの中、 `KOSProcessor` という属性のあるパーツです。

KOSProcessorを宇宙船に取り付けることができたら、Lunchpadに移ります。
MenuBar(画面右端)に`kOS`のiconがありますので、それをクリックしてkOSのメニューを開きましょう。
![kOS.png](https://qiita-image-store.s3.amazonaws.com/0/221366/784c767a-6754-c374-2545-de7c2e8fdaff.png)

ターミナルのアイコンを押すとGame内のターミナルが開くので、そこにコマンドを打ち込んで行くこともできますが、今回は`TELNET`ボタンを押して手元のtelnet teminalから下記のExampleスクリプトを流し込んでみました。

```
print "Launching".
lock steering to heading(90,80).
lock throttle to 1.
stage.
wait until altitude > 5000.
lock steering to heading(90,60).
wait until altitude > 15000.
lock steering to heading(90,45).
wait until altitude > 25000.
lock steering to heading(90,30).
until apoapsis > 80000 {
  print "apoapsis is " + round(apoapsis,0).
  wait 0.
}
lock throttle to 0.
```

どうやらスクリプトが流し込まれた時点で動作が始まってしまったようです。
ざっと中身を読む限り、スロットル全開で打ち上げ、高度が上がる毎に方位を水平に倒して、遠地点高度が80kmに達した時点でエンジンカットオフしてくれるようですね。

ふむ。遠地点付近で再度エンジンを噴射してあげなければ、そのまま地面に真っ逆さまのはずですが、このスクリプトの例では、どうやらその部分は省略されているようですね。なるほど[^9]


## kRPC
[Source Code (github)](https://github.com/krpc/krpc) / [Document](http://krpc.github.io/krpc/index.html) / [Forum](https://forum.kerbalspaceprogram.com/index.php?/topic/62902-131-krpc-remote-procedure-call-server-v042-3rd-november-2017/)
難易度: そこそこ / 言語: C#, C++, Java, Lua, Python, Ruby, Haskell / Editor: お好みのもの

こいつにはVAB/SPHでのパーツ追加は不要です。

Lunchpadに移ったら、networkっぽいiconをクリックします。中央に水平に伸びた線から上に四角が2つ、下に四角1つ生えてるやつです。

![kRPC.png](https://qiita-image-store.s3.amazonaws.com/0/221366/295ec819-53f8-8536-8357-b236caa6e170.png)

kRPCのMenuが出てくるので、`Start Server`を押してあげると、Game側でRPC serverが立ち上がります。

あとは、お好きな言語、お好きな環境でRPC clientを上げて、ゲームに接続してあげましょう。
localhost以外から接続を受け入れたいなら、`add server`からAddressを`Any`にしたserverを追加してあげましょう。

Clientはいくつかの言語に対応していますが、Pythonであれば

```shell-session
# pip install kRPC
```

から

```python
import krpc
conn = krpc.connect(name='Hello World')
vessel = conn.space_center.active_vessel
print(vessel.name)
```

で、 機体の名前が表示されるはずです。

ClientからServerへの接続時、Server側で「なんかRPCで繋ぎに来た子がいるけど相手していいの？」みたいなdialogが出るので、OKしてあげてください。いちいちServer側を触るのがうるさいときは、`Advanced Options`の`Auto-start server`と`Auto-accept new clients`をcheckすると煩わしさが減ります。


さて、kRPCはうれしいことに、機体を制御しようと思うとたくさんのコードを書けます。
ほぼ[documentの example](http://krpc.github.io/krpc/tutorials/launch-into-orbit.html) ですが、高度90kmの周回軌道に乗せようと思うと、下記ぐらいの分量になります。

```python
import math
import time
import krpc

TURN_START_ALTITUDE = 250
TURN_END_ALTITUDE = 45000
TARGET_ALTITUDE = 90000
ASCENT_HEADING = 0

def main():
    conn = krpc.connect(name='Launch into orbit')
    vessel = conn.space_center.active_vessel
    obt_frame = vessel.orbit.body.non_rotating_reference_frame

    # Set up streams for telemetry
    ut = conn.add_stream(getattr, conn.space_center, 'ut')
    altitude = conn.add_stream(getattr, vessel.flight(), 'mean_altitude')
    apoapsis = conn.add_stream(getattr, vessel.orbit, 'apoapsis_altitude')
    time_to_apoapsis = conn.add_stream(getattr, vessel.orbit, 'time_to_apoapsis')
    stage_2_resources = vessel.resources_in_decouple_stage(stage=2, cumulative=False)
    srb_fuel = conn.add_stream(stage_2_resources.amount, 'SolidFuel')

    # Pre-launch setup
    vessel.control.sas = True
    vessel.control.rcs = False
    vessel.control.throttle = 1.0

    # Countdown...
    print("3...")
    time.sleep(1)
    print("2...")
    time.sleep(1)
    print("1...")
    time.sleep(1)
    print("Launch!")

    # Activate the first stage
    vessel.control.activate_next_stage()
    vessel.auto_pilot.engage()
    vessel.auto_pilot.target_pitch_and_heading(90, ASCENT_HEADING)

    # Main ascent loop
    srbs_separated = False
    apoapsis_near = False
    apoapsis_reached = False
    turn_angle = 0

    while True:

        # Gravity turn
        if altitude() > TURN_START_ALTITUDE and altitude() < TURN_END_ALTITUDE:
            print("Gravity turn")

            frac = ((altitude() - TURN_START_ALTITUDE) /
                    (TURN_END_ALTITUDE - TURN_START_ALTITUDE))
            new_turn_angle = frac * 90
            if abs(new_turn_angle - turn_angle) > 0.5:
                turn_angle = new_turn_angle
                vessel.auto_pilot.target_pitch_and_heading(90-turn_angle, ASCENT_HEADING)

        # Separate SRBs when finished
        if not srbs_separated:
            if srb_fuel() < 0.1:
                srbs_separated = True
                print("SRB separation")
                vessel.control.activate_next_stage()

        # Decrease throttle when approaching target apoapsis
        if not apoapsis_reached:
            if apoapsis_near and apoapsis() < TARGET_ALTITUDE:
                apoapsis_reached = True
                print("Target apoapsis reached")
                vessel.control.throttle = 0.0
            elif apoapsis() > TARGET_ALTITUDE*0.9:
                apoapsis_near = True
                print("Approaching target apoapsis")
                vessel.control.throttle = 0.25
        
        if srbs_separated and apoapsis_reached:
            break

    # Wait until out of atmosphere
    print("Coasting out of atmosphere")
    while altitude() < 70500:
        pass

    # Plan circularization burn (using vis-viva equation)
    print("Planning circularization burn")
    mu = vessel.orbit.body.gravitational_parameter
    r = vessel.orbit.apoapsis
    a1 = vessel.orbit.semi_major_axis
    a2 = r
    v1 = math.sqrt(mu*((2./r)-(1./a1)))
    v2 = math.sqrt(mu*((2./r)-(1./a2)))
    delta_v = v2 - v1
    node = vessel.control.add_node(
        ut() + vessel.orbit.time_to_apoapsis, prograde=delta_v)

    # Calculate burn time (using rocket equation)
    F = vessel.available_thrust
    Isp = vessel.specific_impulse * 9.82
    m0 = vessel.mass
    m1 = m0 / math.exp(delta_v/Isp)
    flow_rate = F / Isp
    burn_time = (m0 - m1) / flow_rate

    # Orientate ship
    print("Orientating ship for circularization burn")
    vessel.auto_pilot.reference_frame = obt_frame
    vessel.auto_pilot.target_direction = node.direction(obt_frame)
    vessel.auto_pilot.wait()

    # Wait until burn
    print("Waiting until circularization burn")
    burn_ut = ut() + vessel.orbit.time_to_apoapsis - (burn_time/2.0)
    lead_time = 5
    conn.space_center.warp_to(burn_ut - lead_time)

    # Execute burn
    print("Ready to execute burn")
    while time_to_apoapsis() - (burn_time/2.0) > 0:
        pass
    print("Executing burn")
    vessel.control.throttle = 1.0
    time.sleep(burn_time - 0.1)
    print("Fine tuning")
    vessel.control.throttle = 0.05

    remaining_delta_v = conn.add_stream(getattr, node, "remaining_delta_v")
    min_delta_v = remaining_delta_v()
    point_passed = False
    while remaining_delta_v() > 0.1 and not point_passed:
        if min_delta_v < remaining_delta_v():
            point_passed = True
        else:
            min_delta_v = remaining_delta_v()
    vessel.control.throttle = 0.0
    node.remove()

    print("Launch complete")

if __name__ == "__main__":
    main()
```

たのしくなってきましたね！

## おわりに
みんなももっとKSPすればいいとおもうよ！

---- 
[^1]: [Kerbal](https://wiki.kerbalspaceprogram.com/wiki/Kerbal/ja) さん達。鳴き声がかわいい
[^2]: [Kerbolシステム](https://wiki.kerbalspaceprogram.com/wiki/Kerbol_System/ja)。ちっちゃい
[^3]: Unityが許す限りにおいて
[^4]: あなたの作るロケットが十分効率的で、Delta-V が許せば
[^5]: VanillaではMouse+KeyboardないしGamePadで宇宙船を操縦することになります
[^6]: リリース当初は開発元のSquadがmod配布をホスティングしていましたが、2014年頃にCurseForgeへ移行…したのですが、ぶっちゃけCurseForgeが使いにくいので流行ってない
[^7]: 試したことはない。ゲーム機はWindowsに限る。GPU積んでないマシンは帰ってくれないか
[^8]: この後、宇宙船は無事にMunの地表に激突し、3名のkerbalは病院送りとなりました。近点距離を調整するノードを設定したあとに、実行する指示を忘れていたのが敗因と思われます。宇宙開発の進歩に犠牲はつきものだ
[^9]: またkerbalが病院送りに。再突入は大分熱そうであった