Processingで動かしてみよう！「月軌道遷移ゲーム」

:::note info
この記事は[CanSatチーム「FUSiON」のアドベントカレンダー2023](https://adventar.org/calendars/9518)20日目(12月22日)の記事です.
:::

# はじめに
FUSiONで地上局を担当している”とるか:satellite_orbital:”([@Attitudecontro3](https://twitter.com/Attitudecontro3))です

12/25のクリスマスまでアドベントカレンダーという形で各自メンバーの活動内容や小話を毎日更新していくつもりです.
本イベントを通じてFUSiONやそのメンバーについて知っていただけると嬉しいです.

# 自己紹介的なやつ
https://qiita.com/Attitudecontro3/items/4ca0e3a2465987b1a591

上の記事で書いてるので,興味あれば読んでください.

# 月軌道遷移ゲームについて
このゲームは,自分が高校生の時に作ってプログラミングコンテストに出したやつをそのまま流用しています.(今見ても突っ込みどころ満載ですがそこは温かい目で見たいただけると...)
この記事は,このゲームを通じて「軌道っておもしろいんだな！」と思ってもらうことです.
ついでに，コピペで動かしてみるのでどんなソースコードが動いているかもわかります。
いろいろ勉強になるんじゃないかと思っているのでぜひ環境構築からやってみましょう！

# 環境構築
まず,プログラミング言語は当時のまま「Processing」という言語を用います.

https://www.processing.org/

上のページからダウンロードできるようになっています.ZIPファイルがダウンロードされるので,これをお好みの場所に解凍したら、解凍先で「Processing.exe」ファイルをクリックしてください.そうすると,IDEがひらいてここに直接ソースコードを書きこんで実行できるようになります.
ほかのプログラミング言語と比較してだいぶ楽に環境構築ができると思います！
(あとで画像入れます)

# Hello World!
環境構築を行って,一番最初にやることといえばこれ！
```Processing:Hello_World.pde
print("Hello World!");
```
![HelloWorld.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635973/6ad36582-d130-d288-6930-67c2a717ef69.png)
こんな感じでお手軽にプログラムが書けちゃいます！

このソースコードを保存してみましょう！
[ファイル]>[名前を付けて保存]を選択していくと以下のようなUIが出てくるので,お好みの場所にお好みの名前を付けて保存しましょう！
![名前を付けて保存.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635973/4b2075e2-4c99-39be-ed7b-34e5cb9d6b36.png)
この後に,保存したフォルダをのぞいてみると先ほど名前を付けたフォルダができていると思います.
それをクリックすると「お好みの名前.pde」というファイルがあるので,これを起動してやるとさっきと同じIDE画面のお出ましです！
これで,ソースコードを保存する方法が分かりました！

# Processingの基本
Processingは,JAVA言語をベースに作られた「CGプログラミング」ソフトウェアになっています.
オープンソースで開発されており,無償で利用でき,プログラミングに造詣が深くなくとも簡単にアニメーションCGが作れちゃいます！
アニメーションのプログラムを記述するときのProcessingのコードのひな形は,以下のようになります.
```Processing:ひな形
//1回しか行わないセットアップの処理
void setup(){
   
}

//この中身が何度も実行される
void draw(){
}
```
具体的にアニメーションを動かしてみましょう！
画面上で円を移動させて,壁に当たったら跳ね返るプログラムです！
```Processing:bound.pde
// 初期値を設定しておく変数
float Px = 0.0;// 横軸位置
float Py = 0.0;// 縦軸位置
float Vx = 1.0;// 横軸速度
float Vy = 3.0;// 縦軸速度
float dt = 5.0;// 経過時間
float size = 40;// 円のサイズ

//1回しか行わないセットアップの処理
void setup(){
  size(500,600);//画面サイズ:横軸500px/縦軸600px
  frameRate(60);//1秒間に60回画面を更新する
}

//この中身が何度も実行される
void draw(){
  background(255,255,255);//画面を塗りつぶす R,G,Bの0～255
  fill(0, 0, 0); // 図形塗りつぶし設定(黒)
  noStroke(); // 線無し
  translate(width/2, height/2); // Processingの座標系を中心に移動
  ellipse(Px, Py, size, size); // 円(座標x, 座標y, width, height);
  Px = Px + Vx*dt; // 速度を位置に反映:x軸
  Py = Py + Vy*dt; // 速度を位置に反映:y軸
  // 左の壁を超えたとき
  if( Px < -width/2+size/2){
    Px = -width/2+size/2;
    Vx = -Vx;
  }
  // 右の壁を超えたとき
  if( Px > width/2-size/2){
    Px = width/2-size/2;
    Vx = -Vx;
  } 
  // 上の壁を超えたとき
  if( Py < -height/2+size/2){
    Py = -height/2+size/2;
    Vy = -Vy;
  }
  // 下の壁を超えたとき
  if( Py > height/2-size/2){
    Py = height/2-size/2;
    Vy = -Vy;
  } 
}
```
こういった感じでプログラムを書いていきます.
これも動かしてみるといいと思います！(バージョン:4.3であれば動くはずです.)
こんな感じで「setup()」「draw()」関数を使っていくことになります.

# 月軌道遷移のソースコード
軌道計算ゲームのソースコードを以下に示します.
これをIDEにコピペして保存して実行しましょう!

```Processing:Moon_orbit_transition.pde
PrintWriter file; 
float x, y;

int LABIT_status=0;
int T_flag = 0;
float dt = 0.1;
final float w=1600/2;
final float h= 900/2;

Planet earth, moon;
Artificial probe;

void setup(){
  file = createWriter("orbit_data.csv");
  background(0);
  size(1600, 900);
  frameRate(60);
  start_screen();
}

void draw(){
  orbitPhese();
}

// 月の計算を実施
class Planet{//メンバ
  public PVector a = new PVector(0,0);//加速度ベクトル.
  public PVector V = new PVector(0,0);//速度ベクトル.
  public PVector P = new PVector(0,0);//位置ベクトル.
  public float radius;//半径.
  public float mass;//質量.
  
  PVector work = new PVector(0,0);//汎用ベクトル.
  
  
    Planet(float Px, float Py, float Vx, float Vy, float R, float M){
      //コンストラクタ
      this.P.set(Px,Py);//初期位置決定.
      this.V.set(Vx,Vy);//初期速度決定.
      this.radius=R;//半径決定.
      this.mass=M;//質量決定.
    }
      //メゾット
        //描画関数系
      void Point(int R, int G, int B){//点表示.
        stroke(R, G, B);//引数により色決定
        strokeWeight(5);
        point(this.P.x, this.P.y);//点の座標決定.
      }
      void Ellipse(int R, int G, int B){//円表示.
        fill(R, G, B);//引数により色決定
        stroke(R, G, B);
        ellipse(this.P.x , this.P.y, this.radius*2,this.radius*2);
      }//円の座標,大きさ決定
      
       //計算系
      void Acc(PVector a_p, float a_mass){
        //万有引力の法則と運動方程式～加速度算出.
        float F,dist_p;//計算用変数
        dist_p=this.P.dist(a_p);//相手の星との距離計算.
        F=(a_mass)/sq(dist_p);//発生する加速度.
        work.set(a_p.x-this.P.x, a_p.y-this.P.y);//発生する力の向き.
        work.normalize();//向きの単位化.
        a.set(work);
        a.mult(F);//加速度ベクトル確定.
      }
      void New(){
        //加速度ベクトル～新速度,新位置確定
        work.set(this.a);//汎用ベクトル化.
        work.mult(dt);//速度の変化量を求める.
        this.V.add(work);//新規速度=現在速度＋変化量.
        
        work.set(this.V);//汎用ベクトル化.
        work.mult(dt);//変位を求める.
        this.P.add(work);//新規位置=現在位置＋変位.
      }
}

// 探査機の計算を実施
class Artificial extends Planet{//クラスの継承.
  
  public float u_fuel=1;//Use.噴射質量.
  public float v_fuel=50;//Velocity.噴射速度.
  public float fuel;//燃料
  float delta_V;

  Artificial(float Px, float Py, float Vx, float Vy, float R, float M, float F){
    super(Px, Py, Vx, Vy, R, M);//スーパークラス
    this.fuel=F;//初期燃料規定
  }
   void Acc(PVector one_P, PVector more_P, float one_m, float more_m){
         //加速度のオーバーライド
        float F,dist_p;//計算用変数
        dist_p=this.P.dist(one_P);//相手の星その①との距離計算
        F=(one_m)/sq(dist_p);//発生する加速度1の大きさ
        work.set(one_P.x-this.P.x, one_P.y-this.P.y);//向き
        work.normalize();//正規化
        work.mult(F);
        a.set(work);//惑星その①との間にかかる加速度確定
        
        dist_p=this.P.dist(more_P);//相手の星その②との距離計算
        F=(more_m)/sq(dist_p);//発生する加速度2の大きさ
        work.set(more_P.x-this.P.x, more_P.y-this.P.y);//向き
        work.normalize();//単位化
        work.mult(F);//加速度確定
        a.add(work);//加速度(①＋②)によりこれに働く正式な加速度を求めた
   }
   void thrustar(){
     if(this.fuel>0){
     if(keyPressed==true){//何かキーが押された時
       float b_fuel = this.fuel;//燃料残量保持
       this.fuel = this.fuel - u_fuel;//燃料残量更新
       float delta_V = this.u_fuel/( this.mass + b_fuel );//燃料の相対速度,質量比算出 
       work.set(this.V);//速度ベクトルを取得
       work.normalize();//正規化(単位ベクトル化)
       work.mult(v_fuel*delta_V);//速度の比をかけた。
         if(keyCode==UP){
           this.V.add(work);//UPなら接線速度増加
           T_flag=1;
         }
         if(keyCode==DOWN){
           this.V.sub(work);//dawnなら接線速度減少
           T_flag=-1;
         }
     }
   }
   }
}

// 軌道計算のセットアップ(初期条件)
void start_screen() {
  earth = new Planet(60+w, 0+h, 0, -0.1375, 10, 80000);
  moon = new Planet(550+w, 0+h, 0, 11.5, 5, 1000); 
  probe = new Artificial(0+w, 0+h, 0, -40, 1, 100, 1000);// 月の周りを周回させたいときにコメントアウト
   //probe = new Artificial(580+w, 0+h, 0, 11+5, 5, 1000, 1000);// 月の周りを周回させたいときにコメントアウト会場
}

// 軌道計算全体を実施
int orbitPhese() {
  rectMode(CORNER);
  int RABIT=0;//続行
  fill(0, 30, 50, 10);
  rect(0, 0, width, height);

  textAlign(CORNER);
  fill(255);
  textSize(25);
  text("[Key Guide]", 40, 60);
  text("SPEED UP  :↑", 70, 95);
  text("SPEED DOWN:↓", 70, 130);
  text("RESTART   :R", 70, 165);
  text("BACK MENU :E", 70, 200);

  earth.Acc(moon.P, moon.mass);
  moon.Acc(earth.P, earth.mass);
  probe.Acc(earth.P, moon.P, earth.mass, moon.mass);

  probe.thrustar();

  earth.New();
  moon.New();
  probe.New();
  
  strokeWeight(5);

  earth.Ellipse(0, 200, 200);
  moon.Ellipse(255, 255, 200); 
  probe.Ellipse(255, 0, 255);
  
  csv_loging(earth.P);
  csv_loging(moon.P);
  csv_loging(probe.P);
  csv_loging(probe.V);
  file.println(T_flag);
  T_flag=0;
    
  return RABIT;
}

void csv_loging(PVector V){
  file.print(V.x);
  file.print(",");
  file.print(V.y);
  file.print(",");
  file.print(V.z);
  file.print(",");
}

void keyReleased() 
{
    if (keyCode == 'E'){
      file.flush();
      file.close();
      exit();
    }
    if (keyCode == 'R'){
       background(0);
       start_screen();
    }
} 
```

以上のソースコードをコピペ・保存して実行してみると,以下のような画面が表示される.
キーボードの「↑」を押下すると,探査機が加速されて軌道が大きくなり,「↓」を押すと小さくなっていきます.
また,「R」ボタンでリセット,「E」ボタンでゲームを終了できます.

実際に遊んでみるとこんな感じになります！
![遊んでみた結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635973/6a08438e-ba54-2554-e7b0-4595d2aa56da.png)



また,ソースコードと同じフォルダを見てみると,画像のように「orbit_data.csv」というCSVファイルが保存されています.

![OrbitDataCSV.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635973/67a8a95c-7594-1a40-d10d-ef1b67026f9f.png)


これは,右クリック「プログラムから開く」よりExcelで開くと,位置情報や速度情報のグラフが表示できるようになっています.
データの並びは,右から「惑星位置ベクトル(3D),月位置ベクトル(3D),探査機位置ベクトル(3D),探査機速度ベクトル(3D),UP/NEUTRAL/DOWNを1/0/-1で表現」のデータが表示されるようになっています.
これでデータ整理とか何かができそう？
![データ整理ぽい何か.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635973/64666645-f6c6-a0cf-4ff5-2756a7cc902e.png)



また,初期値を工夫すると月軌道を周回するような絵をかくこともできます.
![月周回png.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635973/a05a095b-3f64-0b72-6e92-d552e8f3c340.png)


制約条件やスコアの設定次第で面白いゲームが作れると思うので皆さんもぜひ挑戦してみてください.

# 最後に
アドベントカレンダー書く時間なさすぎて昔のプログラムを引っ張ってきました...
高校生の時は寝ても覚めてもこのコードのことを考えていたことを思い出して苦笑してます.
何はともあれ,これで面白いと思ってもらえれば幸いです！

あと,もしゲームを作って公開するときとかは,この記事引用していることを明記してほしいです！
それと連絡とかをいただくと泣いて喜びます.

月軌道遷移に成功するとこんな感じですー
![月軌道遷移.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635973/f3eabc5a-b5c8-11a9-0f44-357b67e78f00.png)

![スクリーンショット 2023-12-22 131559.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635973/90f58c8d-8d65-a8a2-7338-855a40e0e23a.png)

# 追記1:P5.jsに対応
以下のソースコードを[p5.js](https://editor.p5js.org/)のエディタ部分にコピペするといい感じに動くかと思います！
(実行画面をクリックすると全画面になるはずです,位置ずれは各自で調整してみてください)
```p5.js
let earth, moon, probe;
let file = [];
let LABIT_status = 0;
let T_flag = 0;
let dt = 0.1;
const w = 1200 / 2;
const h = 900 / 2;

class Planet {
  constructor(Px, Py, Vx, Vy, R, M) {
    this.P = createVector(Px, Py); // 位置ベクトル
    this.V = createVector(Vx, Vy); // 速度ベクトル
    this.a = createVector(0, 0); // 加速度ベクトル
    this.radius = R; // 半径
    this.mass = M; // 質量
    this.work = createVector(0, 0); // 汎用ベクトル
  }

  Point(R, G, B) {
    stroke(R, G, B);
    strokeWeight(5);
    point(this.P.x, this.P.y);
  }

  Ellipse(R, G, B) {
    fill(R, G, B);
    stroke(R, G, B);
    ellipse(this.P.x, this.P.y, this.radius * 2, this.radius * 2);
  }

  Acc(a_p, a_mass) {
    let dist_p = this.P.dist(a_p);
    let F = a_mass / (dist_p * dist_p);
    this.work.set(a_p.x - this.P.x, a_p.y - this.P.y);
    this.work.normalize();
    this.work.mult(F);
    this.a.set(this.work);
  }

  New() {
    this.work.set(this.a);
    this.work.mult(dt);
    this.V.add(this.work);

    this.work.set(this.V);
    this.work.mult(dt);
    this.P.add(this.work);
  }
}

class Artificial extends Planet {
  constructor(Px, Py, Vx, Vy, R, M, F) {
    super(Px, Py, Vx, Vy, R, M);
    this.u_fuel = 1; // 使用する燃料
    this.v_fuel = 50; // 燃料の噴射速度
    this.fuel = F; // 初期燃料
  }

  Acc(one_P, more_P, one_m, more_m) {
    let dist_p = this.P.dist(one_P);
    let F = one_m / (dist_p * dist_p);
    this.work.set(one_P.x - this.P.x, one_P.y - this.P.y);
    this.work.normalize();
    this.work.mult(F);
    this.a.set(this.work);

    dist_p = this.P.dist(more_P);
    F = more_m / (dist_p * dist_p);
    this.work.set(more_P.x - this.P.x, more_P.y - this.P.y);
    this.work.normalize();
    this.work.mult(F);
    this.a.add(this.work);
  }

  thrustar() {
    if (this.fuel > 0) {
      if (keyIsPressed) {
        let b_fuel = this.fuel;
        this.fuel -= this.u_fuel;
        let delta_V = this.u_fuel / (this.mass + b_fuel);
        this.work.set(this.V);
        this.work.normalize();
        this.work.mult(this.v_fuel * delta_V);

        if (keyCode === UP_ARROW) {
          this.V.add(this.work);
          T_flag = 1;
        }
        if (keyCode === DOWN_ARROW) {
          this.V.sub(this.work);
          T_flag = -1;
        }
      }
    }
  }
}

function setup() {
  //createCanvas(1600, 900);
  createCanvas(windowWidth, windowHeight);
  //print(windowWidth);
  //print(windowHeight);
  frameRate(60);
  start_screen();
}

function draw() {
  orbitPhase();
}

function start_screen() {
  earth = new Planet(60 + w, 0 + h, 0, -0.1375, 10, 80000);
  moon = new Planet(500 + w, 0 + h, 0, 11.5, 5, 1000);
  probe = new Artificial(0 + w, 0 + h, 0, -40, 1, 100, 1000);
}

function orbitPhase() {
  rectMode(CORNER);
  fill(0, 30, 50, 10);
  rect(0, 0, width, height);

  textAlign(LEFT);
  fill(255);
  textSize(25);
  text("[Key Guide]", 40, 60);
  text("SPEED UP  :↑", 70, 95);
  text("SPEED DOWN:↓", 70, 130);
  text("RESTART   :R", 70, 165);
  text("BACK MENU :E", 70, 200);

  earth.Acc(moon.P, moon.mass);
  moon.Acc(earth.P, earth.mass);
  probe.Acc(earth.P, moon.P, earth.mass, moon.mass);

  probe.thrustar();

  earth.New();
  moon.New();
  probe.New();

  strokeWeight(5);
  earth.Ellipse(0, 200, 200);
  moon.Ellipse(255, 255, 200);
  probe.Ellipse(255, 0, 255);

  csv_logging(earth.P);
  csv_logging(moon.P);
  csv_logging(probe.P);
  csv_logging(probe.V);

  file.push(T_flag);
  T_flag = 0;
}

function csv_logging(V) {
  file.push(`${V.x},${V.y},${V.z}`);
}

function keyReleased() {
  if (key === 'E') {
    saveStrings(file, 'orbit_data.csv');
    noLoop();
  }
  if (key === 'R') {
    background(0);
    start_screen();
  }
}

//マウスクリックしたときに実行
function mousePressed() {
  let fs = fullscreen();
  fullscreen(!fs);
}

//ウィンドウサイズが変更されたときに実行される関数
function windowResized() {
  // print("ウィンドウサイズの変更");
  resizeCanvas(windowWidth, windowHeight);
}
```