# beatoraja改造計画
大規模ソフトウェアを手探る team11[islands]
## はじめに
この記事は、東京大学電気電子工学科・電子情報工学科(通称EEIC)3年の後期実験の1つ「大規模ソフトウェアを手探る」で行ったことに関するものです。
この実験では、2~3人のチームを組み、オープンソースのソフトを1つ選んで、中身を紐解きながら変更を加えていきます。
## Beatorajaについて
BeatorajaとはBMS形式のファイルを再生するプレイヤーの一つです。ほかに有名なものとしてLunatic rave 2などがあります。
CONAMIの音楽ゲーム、BeatManiaを模したゲームを遊ぶことができます。
Beatorajaはオープンソースで開発されており、開発者は両替士(exch-bms2)です。
現在はBMSプレイヤーの中では最も一般的なものの一つといえる。スキンの開発も盛んで現在も積極的なプルリクやアップデートリリースが行われています。
今回はこのBeatorajaについて、機能修正・追加を行っていきます。
なお、使用言語は主にJavaです。
## やったこと一覧
- [難易度オートソート](#auto-sort)
- [テンキー操作](#numpad)
- [メトロノーム](#metronome)
- [付随するバグの修正](#bug-fix)
※以降、クラス/インターフェースを表記する際、基本的に`bms.player.beatoraja`パッケージ以下にあるものとして、この階層は省略します。(それ以外を扱うことはなかったので)
(例. `bms.player.beatoraja.play.BMSPlayer`→`play.BMSPlayer` )
## 前準備 (ビルド・実行の確認)
本家リポジトリをforkしてローカルにcloneした後、まず最初にそのままでビルド・実行できることを確認しておきます。
今回はAntビルドが使われており、ビルドすると実行用jarファイルが生成されます。また、jarファイルを生成せずとも`MainLoader`クラスから実行することもできました。
ただし、[当時のmaster](https://github.com/exch-bms2/beatoraja/commit/550baf6496fc474334592009fb85fdd21be1458c)はJava8との互換性がなく、コンパイルが通りませんでした。その1つ前のコミットでは問題なくコンパイルされることを確認しています。なお、後にこの問題の[修正PR](https://github.com/exch-bms2/beatoraja/pull/652)がmergeされています。
## <a name="auto-sort">難易度オートソート</a>
### 概要
この修正では、楽曲ソート時に楽曲の名部情報を利用してソート結果が変更されるようにしました。
本修正は開発元githubのissue [難易度オートソートの提案 #624](https://github.com/exch-bms2/beatoraja/issues/624)を実現しようとした試みである。
変更内容は[このリポジトリ](https://doss-gitlab.eidos.ic.i.u-tokyo.ac.jp/islands/beatoraja_eeic/-/tree/feature/music-sort)にある。
最終的に、本変更は元リポジトリに対して[プルリクエスト](https://github.com/exch-bms2/beatoraja/pull/655)を出して、最終的にマージされた。
### ソートアルゴリズムの確認
楽曲選択時に楽曲をソートする機能がある。既に複数種類のソートが存在していてそのソート方法を修正する形で行いました。
ソートに関する処理は`select.BarSorter`に格納されています。
確認すると、Barと定義されたオブジェクト二つを比較する関数を定義して、Bar同士の大小を求めて、それが小さくなる順番にソートをかける実装になっています。
そこで、この大小を決める関数を修正してソート結果が変更されるようにしました。
### 要件の確認
開発元githubのissue [難易度オートソートの提案 #624](https://github.com/exch-bms2/beatoraja/issues/624)をもとに、やりたいことを確認します。
同じ楽曲が並んでいるときにbeginnerやHardなどの難易度の順に並んでいると嬉しい、という話のようです。
ここでは、
- `NAME`ソートで同じ楽曲に対して、難易度のソートを行う
- `LEVEL`ソートで同じLEVELの楽曲について、難易度のソートを行う
修正を行いました。
### 具体的な変更
#### NAMEソート
まずは、NAMEソートについての変更を確認します。
難易度情報は`song.SongData`を参照すると`songData`クラスにint型で`difficulty`という名前で格納されています。
そこで以下のような変更を行いました。
`select.BarSorter`
変更前
```java=31
return o1.getTitle().compareToIgnoreCase(o2.getTitle());
```
```java=32
if((o1 instanceof SongBar && o2 instanceof SongBar)){
//タイトルの比較値を変数で保持
int title_compare;
title_compare = ((SongBar)o1).getSongData().getTitle().compareToIgnoreCase(((SongBar)o2).getSongData().getTitle());
if(title_compare == 0){ //タイトルが一致
return (((SongBar)o1).getSongData().getDifficulty()-((SongBar)o2).getSongData().getDifficulty());
}else{ //タイトルが不一致
return title_compare;
}
}else{
return o1.getTitle().compareToIgnoreCase(o2.getTitle()) ;
}
```
Barには`SongBar`以外のものも存在するので、`SongBar`どうしに対して特殊処理を行うように変更しました。
`SongBar`に格納されている`SongData`を取得して、そこにある`title`を直接取得します。
`SongBar.getSongData()`は`SongBar`に格納された`SongData`を取得する関数、`SongData.getTitle()`はSongDataに格納された`title`のみ(`subtitle`は含まない)を取得する関数で、それぞれの型定義内で定義されています。
これによって、内部処理上`title`が一致していればさらに難易度によるソートが行われるようになりました。
#### LEVELソート
次は`LEVEL`ソートに対する変更を確認します。
変更前
```java=106
return ((SongBar) o1).getSongData().getLevel() - ((SongBar) o2).getSongData().getLevel();
```
変更後
```java=122
//levelが同じ場合はDifficultyでソート
int revelSort=((SongBar) o1).getSongData().getLevel() - ((SongBar) o2).getSongData().getLevel();
if(revelSort==0){
return ((SongBar)o1).getSongData().getDifficulty()-((SongBar)o2).getSongData().getDifficulty();
}else{
return revelSort;
}
```
NAMEソートと同じように、levelによる比較が等しい場合にDifficultyによる比較が行われるようになりました。
これによって、おなじlevel(数字)に対してはDifficultyによるソートが行われるようになりました。
### 限界
NAMEソートには、期待通りのソートがなされないケースが存在します。
それは「`title`が異なっている場合」です。これだけ聞くと当然のことのように思えますが、例を出して説明します。
「HOGEHOGE」という曲があったとしましょう。二つ難易度があるとして、それぞれのTITLEとSUBTITLE、Difficultyが
- 一つ目
- TITLE:HOGEHOGE
- SUBTITLE:[EASY]
- DIFFICULTY:1
- 二つ目
- TITLE:HOGEHOGE
- SUBTITLE:[HARD]
- DIFFICULTY:3
と以上のようになった場合、TITLEが一致しているのでDIFFICULTYによるソートが行われます。
次に、以下のような「FUGAFUGA」という曲を考えてみます。
- 一つ目
- TITLE:FUGAFUGA[EASY]
- DIFFICULTY:1
- 二つ目
- TITLE:FUGAFUGA[HARD]
- DIFFICULTY:3
今度はSUBTITLEの設定がなく、TITLEに飾りをつける形で難易度情報が記述されています。このような場合、TITLEが一致しているとみなされず、難易度ソートが行われません。
これは、仕様の限界で楽曲ファイル(BMSファイル)の設定側の問題といえるので、このようなケースに対するケアは行いませんでした。
## <a name="numpad">テンキー操作</a>
### 概要
このソフトでは、数字キーがホットキーに割り当てられています。
(例: 選曲画面で6を押すとキーコンフィグ画面に移動)
この操作をテンキーでも受け付けるようにしようという変更です。
こちらはissueに上がっていたものではありませんが、実際に操作していて個人的に欲しいなと思った機能です。テンキーを押しては無反応…ということをよくやったので。
### 内部動作の確認
キーボード入力はlibGDXを介して`input.KeyBoardInputProcesseor`で処理され、`input.BMSPlayerInputProcessor`で管理されていました。後者のクラスでは、専用コントローラやMIDIデバイスの入力も管理しているようです。
ここでの管理情報をもとに、各画面用のクラスが、入力状況に応じた処理を毎フレーム行っています。
数字キーの入力は、`BMSPlayerInputProcessor`にある2つのプロパティ`boolean[] numberstate`と`long[] numbertime`から確認できるようになっています。ともに長さ10の配列で、前者は各キーが押されているか、後者はキー状態の最終更新時間(ms)です。また、最終更新時間を0にすることで処理不要(処理済み)を示すこともあるようです。
ついでに、テンキーが他の操作で使われていなそうなのを確認しました。被ってしまったらややこしいので…。
具体的には、`/Keys.NUMPAD_\d/`(in 正規表現)がプロジェクト内で記述されていないことを、IDEのプロジェクト内検索で確認しました。
### 状態管理用のメソッドの整備
普通の数字キーにならって整備していきます。
##### `BMSPlayerInputProcessor`
2つのプロパティ`numpadstate`, `numpadtime`と、それのgetterである`getNumpadState()`, `getNumpadTime()`を追加
##### `KeyBoardInputProcesseor`
- キーコードの配列を追加
```java=28
/**
* テンキー
*/
private final int[] numpads = {
Keys.NUMPAD_0, Keys.NUMPAD_1, Keys.NUMPAD_2, Keys.NUMPAD_3, Keys.NUMPAD_4, Keys.NUMPAD_5,
Keys.NUMPAD_6, Keys.NUMPAD_7, Keys.NUMPAD_8, Keys.NUMPAD_9
};
```
普通の数字キー用の`numbers`プロパティに対応します。
- テンキーの監視を`poll()`に追加
```java=154
for (int i = 0; i < numpads.length; i++) {
final boolean pressed = Gdx.input.isKeyPressed(numpads[i]);
if (pressed != keystate[numpads[i]]) {
keystate[numpads[i]] = pressed;
this.bmsPlayerInputProcessor.numpadstate[i] = pressed;
this.bmsPlayerInputProcessor.numpadtime[i] = presstime;
}
}
```
キーの状態に変更があった場合、それを`MSPlayerInputProcessor`の2プロパティに反映させています。
### 各操作の対応
あとは、各所にある操作に、テンキーを対応させていくのみです。
例えば、選曲画面でのKEYフィルタ切り替え(1キー)に関しては、
`select.MusicSelectInputProcessor`の`input()`において
```java=94
if (numberstate[1] && numtime[1] != 0) {
// KEYフィルターの切り替え
numtime[1] = 0;
select.executeEvent(EventType.mode);
}
```
から
```java=96
if ((numberstate[1] && numtime[1] != 0) || (numpadstate[1] && numpadtime[1] != 0)) {
// KEYフィルターの切り替え
numtime[1] = 0;
numpadtime[1] = 0;
select.executeEvent(EventType.mode);
}
```
と変更しました。
ここ以外にも、
- `config.KeyConfiguration::render`
- `play.ControlInputProcessor::input`
- `result.CourseResult::input`
- `result.MusicResult::input`
- `select.MusicSelector::input`
に同様の変更を加えました。
これで、無事テンキーに対応することができました。
## <a name="metronome">メトロノーム</a>
### 概要
beatorajaには、反復練習を支援するためのプラクティスモードというモードが存在します。そのプラクティスモードで、メトロノームを鳴らしながら練習できるよう、機能を実装しました。
修正を加えたのは主に以下の点です。
- プラクティスモードのメニューの変更
- メトロノームの再生処理
- メトロノーム音源のパス取得
- プラクティスモード周辺のバグ修正
### 要件の確認
メトロノームの要件として、
- リズムに合わせて音が鳴ること
- プラクティスモード内でメトロノームを再生するかどうかを選べること
- メトロノームの音量がプラクティスモード内で調整できること
を定めました。これは機能がいらない人にも今まで通りの機能を提供することと、楽曲によって音圧がまちまちでメトロノームが満足に聞き取れない問題を解決するためです。
### プラクティスモードのメニューの変更
プラクティスモードでは、反復練習を支援するために固有の設定画面が用意されています。
その設定画面で「メトロノームを鳴らすかどうか」「メトロノームの音量」を設定できるようにしました。
設定画面に関する記述は`play.PracticeConfiguration`に格納されています。
設定画面の見た目はdraw関数で定義されています。
`play.PracticeConfiguration`
```java=290
public void draw(Rectangle r, SkinObjectRenderer sprite, long time, MainState state) {
float x = r.x + r.width / 8;
float y = r.y + r.height * 7 / 8;
sprite.draw(titlefont, String.format("START TIME : %2d:%02d.%1d", property.starttime / 60000,
(property.starttime / 1000) % 60, (property.starttime / 100) % 10), x, y, cursorpos == 0 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont, String.format("END TIME : %2d:%02d.%1d", property.endtime / 60000,
(property.endtime / 1000) % 60, (property.endtime / 100) % 10), x, y - 22,cursorpos == 1 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont, "GAUGE TYPE : " + GAUGE[property.gaugetype], x, y - 44,cursorpos == 2 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont, "GAUGE CATEGORY : " + property.gaugecategory.name(), x, y - 66,cursorpos == 3 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont, "GAUGE VALUE : " + property.startgauge, x, y - 88, cursorpos == 4 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont, "JUDGERANK : " + property.judgerank, x, y - 110, cursorpos == 5 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont, "TOTAL : " + (int)property.total, x, y - 132, cursorpos == 6 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont, "FREQUENCY : " + property.freq, x, y - 154, cursorpos == 7 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont, "GRAPHTYPE : " + GRAPHTYPE[property.graphtype], x, y - 176, cursorpos == 8 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont,"METRONOME : "+property.metronome,x,y-198,cursorpos == 9 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont,"METRONOME VOLUME : "+property.metronomevolume,x,y-220,cursorpos == 10 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont, "OPTION-1P : " + RANDOM[property.random], x, y - 242, cursorpos == 11 ? Color.YELLOW : Color.CYAN);
if (model.getMode().player == 2) {
sprite.draw(titlefont, "OPTION-2P : " + RANDOM[property.random2], x, y - 266, cursorpos == 12 ? Color.YELLOW : Color.CYAN);
sprite.draw(titlefont, "OPTION-DP : " + DPRANDOM[property.doubleop], x, y - 290, cursorpos == 13 ? Color.YELLOW : Color.CYAN);
}
```
上記304,305行目にメトロノームの設定部分を追加して、既存の設定部分の表示位置を調整しました。
設定画面でのコマンド受付は`processInput`関数で定義されています。
ここで、右ボタンを押したら音量を+1する、などの処理を設定します。
```java===
case 9:
property.metronome = !(property.metronome);
break;
case 10:
if(property.metronomevolume>0){
property.metronomevolume -= 1;
}
break;
```
上記のように、受け付けられるように分を追加しました。
また、設定画面で設定された内容をプレイヤー側に渡すための`PracticeProperty`クラスもメトロノームに関する部分を追加しました。
```java=
public static class PracticeProperty {
public int starttime = 0;
public int endtime = 10000;
public GaugeProperty gaugecategory;
public int gaugetype = 2;
public int startgauge = 20;
public int random = 0;
public int random2 = 0;
public int doubleop = 0;
public int judgerank = 100;
public int freq = 100;
public double total = 0;
public int graphtype = 0;
public boolean metronome = false;
public int metronomevolume = 30; //パーセント値のint型で持つ
}
```
これで、設定画面にメトロノームの記述が追加されました。
### メトロノームの再生処理
再生処理は、
1. 音を鳴らすタイミングを判断する
2. 音を鳴らす
ができればOKです。ということで、これを担当するクラス`play.Metronome`を作成しました。
このクラスは、メンバメソッドとして
- `setEnabled()` メトロノームのON/OFFを切り替える
- `update()` 毎フレームの更新(タイミング判断と音の再生)
を持っています。
さて、1. のタイミング判断ですが、こちらは`play.RhythmTimerProcessor`を活用していきます。このクラスはもともと、PMSの1/4小節ごとにノーツを拡大する機能用に作られたもののようです。 <span style="font-size: 70%">※PMS: BMSから派生したフォーマット </span>
このクラスには`sections`と`quarterNote`というメンバ変数があり、それぞれ今再生中の小節番号と1/4小節の番号を表しています。つまり、`RhythmTimerProcessor`の更新後にこれが変化したら、音を鳴らすタイミングだと判定できます。
ただ、この2つのpublicなgetterが存在しなかったので作成しておきます。
そして、2. の音の再生です。こちらは`audio.AudioDriver`の`play()`を通じて簡単に実現できます。
ということで、あとは上記メニューで追加されたプロパティを参照しつつ実装して以下のようになりました。
<details>
<summary>折りたたみ (Metronome)</summary>
```java=6
public class Metronome {
private boolean enabled; // 練習モードか否か等にもよるので、configとは別管理。
private final PracticeConfiguration config;
private final RhythmTimerProcessor rhythm;
private final AudioDriver audio;
private int lastSections = 0;
private int lastQuarterNote = 0;
private String downbeatPath="defaultsound/m-down.wav";//ハードコード
private String upbeatPath="defaultsound/m-up.wav";
public static final int SOUND_M_DOWN = 0;
public static final int SOUND_M_UP = 1;
public Metronome(BMSPlayer main, boolean enabled) {
this.rhythm = main.getRhythmTimerProcessor();
this.audio = main.main.getAudioProcessor();
this.config = main.getPracticeConfiguration();
this.enabled = enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
// rhythmのアップデート後に呼ぶ
public void update() {
if (enabled && rhythm != null) {
float volume = config.getPracticeProperty().metronomevolume / 100f;
if (rhythm.getSections() > lastSections) {
audio.play(downbeatPath, volume, false);
}else if (rhythm.getQuarterNote() > lastQuarterNote) {
audio.play(upbeatPath, volume, false);
}
lastSections = rhythm.getSections();
lastQuarterNote = rhythm.getQuarterNote();
}
}
}
```
</details>
なお、ここで登場した`BMSPlayer`クラスはその名の通りプレイヤー本体です。
そして、音源ファイルのパスがハードコードされていますが、次はそちらの改善です。
### メトロノーム音源のパス取得
beatorajaでは、諸々の効果音は事前に設定されたファイル名で指定されたディレクトリに格納することで自動的に使用される仕様があります。そこで、メトロノームの音源についてもほかの音源同様に扱えるようにしました。
指定ディレクトリは大まかに二種類あってハードコードで指定された`defaultsound`ディレクトリと、起動前の設定で変更可能な`sound`ディレクトリです。`sound`ディレクトリのほうは、若干仕様が複雑です。`sound`ディレクトリの中に効果音の音源がまとまったディレクトリを用意します(スキン製作者や音源製作者がまとめて用意することが多い)。beatoraja側は`sound`ディレクトリにあるディレクトリのうち一つをランダムで選んで、そこにある音源を再生する仕様になっています。
`sound`ディレクトリに音源がないかを探して無い場合は`defaultsound`ディレクトリに格納されたものを再生する、という仕組みがあらかじめ備わっているので、その仕組みを拝借して実装します。
ちなみに、メトロノームの強拍は`m-down.wav`、弱拍は`m-up.wav`としました。mはメトロノームの頭文字、down,upはそれぞれdownbeat,upbeatからとっています。強拍と弱拍が逆のように思われるかもしれませんが、あっています。
`play.Metronome`
```java=29
downbeatPath=main.main.getCurrentState().getSoundPaths("m-down.wav", MainState.SoundType.SOUND)[0];
upbeatPath=main.main.getCurrentState().getSoundPaths("m-up.wav", MainState.SoundType.SOUND)[0];
```
これによって、指定されたファイル名の音源が取得されます。ここで取得したパスを、既に実装した再生処理に渡すことで、準備したメトロノーム音源が再生されるようになります。
### <a name="bug-fix">プラクティスモード周辺のバグ修正(`RhythmTimerProcessor`)</a>
さて、以上でめでたくメトロノームが実現したわけですが、いくつかバグが…。
プラクティスモードは、[概要](#概要2)でも触れたように「反復」練習を支援してくれるのですが、一度再生したところまではメトロノームが鳴らないというバグが存在しました。逆に、初めて再生する部分の途中から再生すると、メトロノームは暴走しました。(1つめ)
そして、再生速度を変える機能も備わっているわけですが、これを変えても暴走したり鳴らなかったり。(2つめ)
これらの原因を突き詰めると、鳴らすタイミングの判定に非常に役立った`RhythmTimerProcessor`のバグにたどり着きました。
#### 1つめ
`RhythmTimerProcessor`では、各小節・1/4小節の境目の時刻をプレイ画面の初期化とともに作成し、更新時にそれを超えたら番号を+1するという処理を行っています。
しかし、スタート位置を設定する機能がないため、反復練習時や途中再生時に不具合が生じていました。
ということで、そのためのメソッドを加えます。~~命名センスのなさ~~
```java=85
public void setAtStart(BMSPlayer player, int freq) {
final long now = player.main.getNowTime();
final long micronow = player.main.getNowMicroTime();
rhythmtimer = micronow;
player.main.setMicroTimer(TIMER_RHYTHM, rhythmtimer);
nowQuarterNoteTime = now;
sections = Arrays.binarySearch(sectiontimes, player.main.getNowMicroTime(TIMER_PLAY) * freq / 100) + 1;
if (sections <= 0) sections *= -1;
if (quarterNoteTimes.length != 0) {
quarterNote = Arrays.binarySearch(quarterNoteTimes, player.main.getNowMicroTime(TIMER_PLAY) * freq / 100) + 1;
if (quarterNote <= 0) quarterNote *= -1;
}
}
```
曲のスタート位置はプレイヤー本体である`BMSPlayer`クラスを通じて、`PLAY`タイマーから取得しています。その位置がどの小節・1/4小節にあたるかを二分探索で求めています。
そして、このメソッドを`BMSPlayer`内で、再生準備完了状態での処理として呼び出します。これはTIMER_PLAYが設定されたあとに行うことに注意します。
```java=611
main.setMicroTimer(TIMER_PLAY, micronow - starttimeoffset * 1000);
main.setMicroTimer(TIMER_RHYTHM, micronow - starttimeoffset * 1000);
rhythm.setAtStart(this, practice.getPracticeProperty().freq);
```
これで1つめの修正完了です。
#### 2つめ
こちらは整数除算に起因するバグでした。
`RhythmTimerProcessor::update`内で小節の境目を超えるか判定する際、
```java
(sectiontimes[sections] * (100 / freq)) <= player.main.getNowMicroTime(TIMER_PLAY)
```
という条件式が書かれていました。再生速度`freq`はパーセント表記のint値(50~200)ですので、左辺の計算時に、先に`100/freq`が整数除算によって0,1,2に丸められてしまいます。括弧を外したら無事解決しました。
これにて、ようやくメトロノームの完成です。(やったー)
### プルリクエスト
本メトロノーム機能は、開発元のブランチに対して[プルリクエスト](https://github.com/exch-bms2/beatoraja/pull/657)を提出しています(2021/11/08現在OPEN状態)。
完全な新機能なこともあり即座に導入されるかはわかりませんが、引き続きmasterへのマージそしてリリース版への追加を目指していきたいと思います。
## プルリク準備について
開発はGitLabで行っていたものの、本家はGitHubなので、プルリク前にまずGitHubに上げる必要がありました。また、プルリク時の最新masterからブランチが生えているようにします。
1. GitHub上で本家をfork
2. ローカルにclone
3. GitLabの開発リポジトリをremoteに追加
`git remote add gitlab <gitlabリポジトリ>`
4. `git fetch gitlab`
5. 各ブランチにcheckoutして、ローカルにもってくる
6. 最新masterから開発ブランチが生えるようにrebase
rebase前は、過去のmasterからdevelopが生え、そこから各featureブランチが生えている。
`git rebase --onto master develop feature/hoge`
(`feature/hoge`の`develop`より先を`master`から生やす)
<div class="column-one">
<div class="column-left">
rebase前
<img src="https://i.imgur.com/aeOCAgn.jpg" title="rebase前" />
</div>
<div class="column-right">
rebase後
<img src="https://i.imgur.com/TdTD9NT.jpg" title="rebase後" />
</div>
</div>
※画像は [gitgraph.js](https://github.com/nicoespeon/gitgraph.js/tree/master/packages/gitgraph-js) で生成しました
7. GitHubにpush
今回は基本的にfast-forwardで行けるが、force push時は`--force-with-lease`を使うと事故が減るらしい
→あとはプルリク!
## さいごに (感想)
#### 中島
おそらく最もアツイBMSプレーヤーであるところのbeatorajaを自分の手でいじることができて楽しかったです。全チームの中でも異色の「ゲーム」をテーマにしたのですが、進捗が遊んで確かめられるのは想像以上にモチベーションにつながりました。~~授業中にやるBMSは楽しかったです。~~
正直な話今までモジュールに分けることのありがたみを今までそんなに感じたことがなかったです。しかし、今回コードを読む段階でモジュール化されていることで読む場所のあたりがつけやすく、コードの役割がわかりやすくなったので、その意義をより実感するきっかけになりました。
#### 平島
Beatorajaは初めて触れたのですが、音ゲー自体は遊んでいるので、今回楽しんで開発できました。~~とはいえキーボード操作難しいです…というのはさておき。~~
OSS開発のきっかけになったらと思ってこの実験を希望してみましたが、やはり良い取っ掛かりになりました。エディタやPython自体をいじっている他の班に比べ、複雑さはあまりないとは思いますが、その分実装した機能の数は稼げたかなと感じます。
中島も触れたように、役割分担によってわかりやすく書かれていて、普段からよ〜く悩む「いかに読みやすく、書きやすく書くか」の1つの参考にもなりそうです。
gitの使い方も、特にrebaseは使うのが初めてで、まだ知らない機能がありそうだなと感じています(stashもようやく慣れてきました)。チームで開発するとなると必須でしょうし、より使いこなせるようにしたいです。
<span style="font-size:75%">余談ながら、Javaは私が初めて触った言語というのもあり好きな言語の一つです。しかしKotlinに移ってだいぶたった今、Javaの面倒さをより強く感じています。JavaもJavaで便利になっているんでしょうけれど…。</span>
<style>
.column-left{
float: left;
width: 47.5%;
text-align: left;
}
.column-right{
float: right;
width: 47.5%;
text-align: left;
}
.column-one{
float: left;
width: 100%;
text-align: left;
}
</style>