ポケモンSVの孵化自動化コードは以前にも作ったのですが、改良版ができたため公開します。
関連記事
ポケモンSVにおける当ブログでは初の自動化コードです!
今回はボックスのタマゴをひたすらに孵化していくコードを作りました。(タマゴ受け取り部分は一旦おあずけです)
ポケモンSVではマイコンでの自動化がなかなか難しいのですが、ある[…]
この記事をご覧になる前に
マイコンをまだ導入していない方は、以下の記事を参考に導入してみてください。このブログでは、『Arduino Leonardo』というマイコンを使っているため、異なる種類のものではうまく動かない可能性があります。
関連記事
マイコンと呼ばれるものをご存知でしょうか?
Switchに接続するだけで様々な作業を "自動" で行ってくれるというものです。
導入すれば作業が楽になるだけでなく、寝てる間に色々稼ぐこともできちゃう便利なアイテムなわけですね!
[…]
今回のコードは「NintendoSwitchControlLibrary」を使用しています。まだダウンロードされていない方は、解凍したフォルダを『Arduino』フォルダの中にある『libraries』フォルダの中にコピーしておいてください。
GitHub
A library for microcontrollers that uses Arduino to automate…
おそらくですが、今回のコードはNintendoSwitchControlLibraryのバージョンが1.3以降でないと動かないと思われます。
2022年8月末に1.3をリリースしていますので、それ以前に導入されている方はライブラリの更新をお願いいたします。
コード利用における注意点!
まず最初に大事なことを書いておくと、今回のコードは完全な放置はできません!!!
ポケモンSVは処理落ちがあったり、それによってコマンドに漏れが生じることがあります。
また、今回のコードに限らずなのですが、エラー落ちが起きる可能性もあります。ポケモンSVは特にエラーが発生しやすいという話を聞いているので、そういった観点でもある程度見張れる状態での使用をおすすめいたします。
エラー落ちした場合、ホーム画面でどんな操作をするかわかりません。e-shopで勝手にソフトを購入したり、ポケモンのセーブデータの削除、Switchの初期化などを行ってしまう可能性も0ではないです。放置は自己責任でお願いいたします。
ソースコード
実際に書いたコードはこちらになります。
- 旧コードはこちら
-
#include <NintendoSwitchControlLibrary.h>
const int BOX_COUNT = 5;
const int REPORT_OPTION = 0;
const int CUSTOM_INPUT_TIME = 200;
const int BOX_COLUMN_LENGTH = 6;
const int BASE_PUSH_BUTTON_TIME_MS = 1000;
const int BASE_CURSOR_OPERATION_TIME_MS = 200;
const int HALF_CURSOR_OPERATION_TIME_MS = 100;
const int MENU_OPEN_OR_CLOSE_TIME_MS = 1500;
int loopCount = 0;
void pushButtonCustom(uint16_t button, unsigned long delay_time, unsigned int loop = 1) {
for (unsigned int i = 0; i < loop; i++) {
SwitchControlLibrary().pressButton(button);
SwitchControlLibrary().sendReport();
delay(CUSTOM_INPUT_TIME);
SwitchControlLibrary().releaseButton(button);
SwitchControlLibrary().sendReport();
delay(delay_time);
}
delay(CUSTOM_INPUT_TIME);
}
void pushHatCustom(uint8_t hat, unsigned long delay_time, unsigned int loop = 1) {
for (unsigned int i = 0; i < loop; i++) {
SwitchControlLibrary().pressHatButton(hat);
SwitchControlLibrary().sendReport();
delay(CUSTOM_INPUT_TIME);
SwitchControlLibrary().releaseHatButton();
SwitchControlLibrary().sendReport();
delay(delay_time);
}
delay(CUSTOM_INPUT_TIME);
}
void sleepGame() {
holdButton(Button::HOME, 1500);
pushButton(Button::A, BASE_PUSH_BUTTON_TIME_MS);
}
void writeReport() {
pushButton(Button::X, MENU_OPEN_OR_CLOSE_TIME_MS);
pushButton(Button::R, 1500);
pushButton(Button::A, 4000);
pushButton(Button::A, BASE_PUSH_BUTTON_TIME_MS);
pushButton(Button::B, MENU_OPEN_OR_CLOSE_TIME_MS);
}
void openBox(bool is_init = false) {
pushButton(Button::X, MENU_OPEN_OR_CLOSE_TIME_MS);
if (is_init) {
holdHat(Hat::RIGHT, BASE_CURSOR_OPERATION_TIME_MS);
holdHat(Hat::UP, 2000);
pushHat(Hat::DOWN, BASE_CURSOR_OPERATION_TIME_MS);
}
pushButton(Button::A, 4000);
}
void closeBox() {
pushButton(Button::B, 3000);
pushButton(Button::B, MENU_OPEN_OR_CLOSE_TIME_MS);
}
void putInPokemons(int box_line) {
pushHatCustom(Hat::LEFT, HALF_CURSOR_OPERATION_TIME_MS);
pushHatCustom(Hat::DOWN, HALF_CURSOR_OPERATION_TIME_MS);
pushButtonCustom(Button::MINUS, BASE_PUSH_BUTTON_TIME_MS);
holdHat(Hat::DOWN, 1000);
pushButtonCustom(Button::A, BASE_CURSOR_OPERATION_TIME_MS);
pushHatCustom(Hat::RIGHT, BASE_CURSOR_OPERATION_TIME_MS + 100, box_line);
pushHatCustom(Hat::UP, BASE_CURSOR_OPERATION_TIME_MS);
pushButtonCustom(Button::A, BASE_CURSOR_OPERATION_TIME_MS);
if (box_line == BOX_COLUMN_LENGTH) {
pushButtonCustom(Button::R, BASE_CURSOR_OPERATION_TIME_MS);
if (loopCount == BOX_COUNT - 1) return;
pushHatCustom(Hat::RIGHT, HALF_CURSOR_OPERATION_TIME_MS);
}
pushHatCustom(Hat::RIGHT, BASE_CURSOR_OPERATION_TIME_MS);
}
void pullOutEggs(int box_line) {
pushButtonCustom(Button::MINUS, BASE_PUSH_BUTTON_TIME_MS);
holdHat(Hat::DOWN, 1000);
pushButtonCustom(Button::A, BASE_CURSOR_OPERATION_TIME_MS);
pushHatCustom(Hat::LEFT, BASE_CURSOR_OPERATION_TIME_MS + 100, (box_line % BOX_COLUMN_LENGTH) + 1);
pushHatCustom(Hat::DOWN, BASE_CURSOR_OPERATION_TIME_MS);
pushButtonCustom(Button::A, BASE_CURSOR_OPERATION_TIME_MS);
}
void doBoxOperations(int box_line) {
openBox();
putInPokemons(box_line);
if (!(box_line == BOX_COLUMN_LENGTH && loopCount == BOX_COUNT - 1)) {
pullOutEggs(box_line);
}
if (box_line == BOX_COLUMN_LENGTH) {
delay(4000);
pushButton(Button::B, 100, 3);
}
closeBox();
}
void switchRide() {
pushButton(Button::PLUS, 10, 6);
delay(BASE_PUSH_BUTTON_TIME_MS);
}
void hatchAllEggs(unsigned long run_time_ms) {
SwitchControlLibrary().moveLeftStick(Stick::MAX, Stick::NEUTRAL);
SwitchControlLibrary().moveRightStick(Stick::MIN, Stick::NEUTRAL);
SwitchControlLibrary().sendReport();
delay(500);
run_time_ms -= 20500;
while (600 <= run_time_ms) {
SwitchControlLibrary().pressButton(Button::A);
SwitchControlLibrary().sendReport();
delay(100);
SwitchControlLibrary().releaseButton(Button::A);
SwitchControlLibrary().sendReport();
delay(500);
run_time_ms -= 600;
}
SwitchControlLibrary().moveLeftStick(Stick::NEUTRAL, Stick::NEUTRAL);
SwitchControlLibrary().moveRightStick(Stick::NEUTRAL, Stick::NEUTRAL);
SwitchControlLibrary().sendReport();
delay(100);
pushButton(Button::A, 500, 35);
}
void setup() {
pushButton(Button::LCLICK, 500, 5);
switchRide();
pushButton(Button::L, 2000);
openBox(true);
pullOutEggs(0);
closeBox();
}
void loop() {
if (loopCount == BOX_COUNT) {
sleepGame();
exit(0);
}
for (int box_line = 1; box_line <= BOX_COLUMN_LENGTH; box_line++) {
hatchAllEggs(260000);
doBoxOperations(box_line);
}
if (REPORT_OPTION == 1) {
writeReport();
}
loopCount++;
}
2023年4月24日追記:Ver1.3.0のリリースに合わせて更新しました。それ以前からのバージョンからも不安定な面があったのですが、今回の対応によりかなり安定感が向上していると思います。一方で、パフォーマンスに関しては1ボックスあたり5秒程度多く時間がかかるようになってしまっていますが、ご理解いただければと思います。
2023年9月14日追記:Ver2.0.1用に更新しました。変更点は最初にマイコンを認識させるためのコマンドがLボタン押し込みになっていましたが、アップデートによりこちらのボタンに口笛が割り当てられるようになったためBボタンに変更しました。それ以外の中身は変更を加えておりません。小さな修正のため、旧コードは残さずそのまま上書きして公開しています。
- 2023年4月24日の更新内容(気になる方は展開してみてください)
- ・一番大きな変化として、ボックスを開く際の待機時間を1秒伸ばしました。仮説ではあるのですが、ボックスの開く時間はボックスの中身が影響する(ボックス操作オンリーで動作確認していて動いていたコードが実際に孵化を挟むことで不安定になることを確認したため)という点、また、これまた推測ではあるのですがボックスの読み込み時間は個体差が出やすい部分かと思われるので、結構余裕をもたせることにしました。
・ケーブルやハブの品質によっては初回のライドができないまま始まってしまう報告があったため、ライドまでの時間を少し伸ばしました。
・ボックス操作時に稀にコマンドの入力漏れが起きてしまう事象、また、逆にカーソル入力が多くなってしまう事象の発生を抑えました。(ボックス操作オンリーのデバッグで200BOX分、実際の孵化も含めたデバッグでも50BOX分ほど動作確認を行っていますが、少なくとも筆者の環境ではこれまで一度も再現していないです。しかし、他の環境ではまだ再現する可能性はあると思っています。)
・稀にポケモンを掴むことに失敗していたことを確認したため、猶予時間をわずかに伸ばしました。
・稀にボックスを閉じることに失敗していたことを確認したため、予備の処理を追加しました。これにより不要になった処理を一部削除しました。また、副作用として、万一ループ破綻した場合であっても、ただ孵化できずにボックスが進んでいくだけとなる動きとなる可能性が上がっており、逆にボックスを荒らしたり着せ替えを開いたりする可能性は下がっています。
・ボックスを開く際のボタン操作やレポート時のボタン操作でも入力漏れが起きにくくなるようにしました。(元からこのケースでの入力漏れは確認できていなかったのですが、副作用もないためついでに対応した形となります。)
・ポケモンを引き取る際に最短ルートで引き取るようにすることで時短になるようにしました。(理論上は預ける際も同様のことができるのですが、預ける際にかかっている時間がいい具合に遅延してくれている可能性を考慮すると、余計なことはしたくなかったので引取時のみの対応としています)これにより、ボックスを開く待機時間を1秒伸ばした部分以外の遅延は相殺できていると思います。
・次のボックスを開いた際の遅延タイミングをボックスを閉じる直前から、次のボックスを開いた直後に変更しました。これにより、ポケモンの引き取りがやや不安定だった問題を解決しています。また、この遅延時間も少し短くしました。このコードの実行中は基本的にタマゴしかない想定であり、タマゴのみであれば読み込みは早いためです。(元々過剰なくらいだったのでもっと削れるかもしれないですが、1BOX中1回しか行わない処理のため、ここを削ってもパフォーマンスに大きく影響がでないことから最小限に留めています。)
#include <NintendoSwitchControlLibrary.h>
const int BOX_COUNT = 5;
const int REPORT_OPTION = 0;
const int CUSTOM_INPUT_TIME = 200;
const int BOX_COLUMN_LENGTH = 6;
const int BASE_PUSH_BUTTON_TIME_MS = 1000;
const int BASE_CURSOR_OPERATION_TIME_MS = 200;
const int HALF_CURSOR_OPERATION_TIME_MS = 100;
const int GRAB_POKEMONS_TIME_MS = 230;
const int MENU_OPEN_OR_CLOSE_TIME_MS = 1300;
int loopCount = 0;
void pushButtonCustom(uint16_t button, unsigned long delay_time, unsigned int loop = 1) {
for (unsigned int i = 0; i < loop; i++) {
SwitchControlLibrary().pressButton(button);
SwitchControlLibrary().sendReport();
delay(CUSTOM_INPUT_TIME);
SwitchControlLibrary().releaseButton(button);
SwitchControlLibrary().sendReport();
delay(delay_time);
}
delay(CUSTOM_INPUT_TIME);
}
void pushHatCustom(uint8_t hat, unsigned long delay_time, unsigned int loop = 1) {
for (unsigned int i = 0; i < loop; i++) {
SwitchControlLibrary().pressHatButton(hat);
SwitchControlLibrary().sendReport();
delay(CUSTOM_INPUT_TIME);
SwitchControlLibrary().releaseHatButton();
SwitchControlLibrary().sendReport();
delay(delay_time);
}
delay(CUSTOM_INPUT_TIME);
}
void sleepGame() {
holdButton(Button::HOME, 1500);
pushButtonCustom(Button::A, BASE_PUSH_BUTTON_TIME_MS - 200);
}
void writeReport() {
pushButtonCustom(Button::X, MENU_OPEN_OR_CLOSE_TIME_MS);
pushButtonCustom(Button::R, 1300);
pushButtonCustom(Button::A, 3800);
pushButtonCustom(Button::A, BASE_PUSH_BUTTON_TIME_MS - 200);
pushButtonCustom(Button::B, MENU_OPEN_OR_CLOSE_TIME_MS);
}
void openBox(bool is_init = false) {
pushButtonCustom(Button::X, MENU_OPEN_OR_CLOSE_TIME_MS);
if (is_init) {
holdHat(Hat::RIGHT, BASE_CURSOR_OPERATION_TIME_MS);
holdHat(Hat::UP, 2000);
pushHatCustom(Hat::DOWN, HALF_CURSOR_OPERATION_TIME_MS);
}
pushButtonCustom(Button::A, 4800);
}
void closeBox() {
SwitchControlLibrary().pressButton(Button::B);
SwitchControlLibrary().sendReport();
delay(120);
SwitchControlLibrary().releaseButton(Button::B);
SwitchControlLibrary().sendReport();
delay(80);
pushButtonCustom(Button::B, 2800);
pushButtonCustom(Button::B, MENU_OPEN_OR_CLOSE_TIME_MS);
}
void putInPokemons(int box_line) {
pushHatCustom(Hat::LEFT, HALF_CURSOR_OPERATION_TIME_MS);
pushHatCustom(Hat::DOWN, HALF_CURSOR_OPERATION_TIME_MS);
pushButtonCustom(Button::MINUS, BASE_PUSH_BUTTON_TIME_MS);
holdHat(Hat::DOWN, 1000);
delay(100);
pushButtonCustom(Button::A, GRAB_POKEMONS_TIME_MS);
for (int i = 0; i < box_line; i++) {
pushHatCustom(Hat::RIGHT, BASE_CURSOR_OPERATION_TIME_MS);
}
pushHatCustom(Hat::UP, BASE_CURSOR_OPERATION_TIME_MS);
pushButtonCustom(Button::A, GRAB_POKEMONS_TIME_MS);
if (box_line == BOX_COLUMN_LENGTH) {
pushButtonCustom(Button::R, BASE_CURSOR_OPERATION_TIME_MS);
delay(3000);
if (loopCount == BOX_COUNT - 1) return;
pushHatCustom(Hat::RIGHT, HALF_CURSOR_OPERATION_TIME_MS);
}
pushHatCustom(Hat::RIGHT, BASE_CURSOR_OPERATION_TIME_MS);
}
void pullOutEggs(int box_line) {
pushButtonCustom(Button::MINUS, BASE_PUSH_BUTTON_TIME_MS);
holdHat(Hat::DOWN, 1000);
delay(100);
pushButtonCustom(Button::A, GRAB_POKEMONS_TIME_MS);
const uint8_t move_direction = (box_line <= 2 || box_line == 6) ? Hat::LEFT : Hat::RIGHT;
const int move_count = (box_line <= 2 || box_line == 6) ? (box_line % BOX_COLUMN_LENGTH) + 1 : BOX_COLUMN_LENGTH - box_line;
for (int i = 0; i < move_count; i++) {
pushHatCustom(move_direction, BASE_CURSOR_OPERATION_TIME_MS);
}
pushHatCustom(Hat::DOWN, BASE_CURSOR_OPERATION_TIME_MS);
pushButtonCustom(Button::A, GRAB_POKEMONS_TIME_MS);
}
void doBoxOperations(int box_line) {
openBox();
putInPokemons(box_line);
if (!(box_line == BOX_COLUMN_LENGTH && loopCount == BOX_COUNT - 1)) {
pullOutEggs(box_line);
}
closeBox();
}
void switchRide() {
pushButton(Button::PLUS, 10, 6);
delay(BASE_PUSH_BUTTON_TIME_MS);
}
void hatchAllEggs(unsigned long run_time_ms) {
SwitchControlLibrary().moveLeftStick(Stick::MAX, Stick::NEUTRAL);
SwitchControlLibrary().moveRightStick(Stick::MIN, Stick::NEUTRAL);
SwitchControlLibrary().sendReport();
delay(500);
run_time_ms -= 20500;
while (600 <= run_time_ms) {
SwitchControlLibrary().pressButton(Button::A);
SwitchControlLibrary().sendReport();
delay(100);
SwitchControlLibrary().releaseButton(Button::A);
SwitchControlLibrary().sendReport();
delay(500);
run_time_ms -= 600;
}
SwitchControlLibrary().moveLeftStick(Stick::NEUTRAL, Stick::NEUTRAL);
SwitchControlLibrary().moveRightStick(Stick::NEUTRAL, Stick::NEUTRAL);
SwitchControlLibrary().sendReport();
delay(100);
pushButton(Button::A, 500, 35);
}
void setup() {
pushButton(Button::B, 500, 8);
switchRide();
pushButtonCustom(Button::L, 1800);
openBox(true);
pullOutEggs(0);
closeBox();
}
void loop() {
if (loopCount == BOX_COUNT) {
sleepGame();
exit(0);
}
for (int box_line = 1; box_line <= BOX_COLUMN_LENGTH; box_line++) {
hatchAllEggs(260000);
doBoxOperations(box_line);
}
if (REPORT_OPTION == 1) {
writeReport();
}
loopCount++;
}
設定の確認
基本は前回のコードと同じなのですが、若干設定が減っています。
- 「セルクルタウン・西」から北に向かった場所にある、オリーブころがし場の柵の中にいること
- ポケモンが連れ歩き状態でないこと
- ライド状態でないこと
- 手持ちが「ほのおのからだ」の特性持ちのポケモン1匹のみであること
- 孵化させたいタマゴの預けてあるボックスが最初に開くこと(複数ボックス孵化させる場合は、ボックスが連続するような配置にしておいてください)
- 孵化させるボックスにタマゴを敷き詰めてあること
- オフライン状態であること
- 本体に装着された状態のジョイコン以外にコントローラーが接続されていないこと
- 「設定」から「話の速さ」を「はやい」に、「ニックネーム登録」を「しない」にしておくこと
重要なものをいくつか解説します!
開始位置について
「セルクルタウン・西」から北に向かったところにある「オリーブころがし場」(この場所に正式な名前ほしい)が今回の舞台です! ジムチャレンジをやったところですね!
この中であればどこからスタートしても大丈夫な認識なのですが、ごく稀に柵を飛び越える事象を確認しているため、中心で動かすのよいと思います。
また、中央のオリーブですが、端っこの方に動かしておくとなお良いです! もしオリーブを柵の外に飛ばしてしまっても中央に復活するだけで暗転等はないため、そこは安心してください!!

ボックスの状態について
タマゴは敷き詰めておいてください!
タマゴが埋まっていなくても動作するとは思うのですが、ボックス操作が若干不安定になる可能性があります。
オプションについて
コードの26行目、29行目がオプションになります。
26行目の「1」の部分が孵化させるボックス数になります。5にすると5ボックス、10にすれば10ボックス孵化させる動きをします。
29行目は1ボックス分の孵化毎にレポートを書くかどうかのオプションになります。SVではエラー落ちが多いと聞いているので、仮にエラー落ちした場合でも被害を最小限に抑えられるよう用意しています。
デフォルトでは無効にしているので、有効にする場合は「0」を「1」にしてください。
数値の編集は必ず半角で行っていただくようお願いいたします。
前回のコードと比較した改善点について
改善点として、以下のループの破綻パターンを潰せています。
- 稀に灯台から落ちてしまう
- キャモメとの戦闘後、稀にハシゴを降りてしまう
灯台から落ちてしまうのは認知していたのですが、キャモメとの戦闘後に灯台の中心に向かってしまいハシゴを降りてしまう事象を報告いただいていました。
今回のコードで走る場所ではポケモンが一切出ない(はず?)のため、これらのループ破綻パターンを解消しています。
それにより、炎の体ポケモンの選定がなくなったことや、孵化したポケモンに経験値が入ってしまう事象も解消できるようになりました!
オリーブころがし場は長時間動かす場合に「障害物にぶつかった際にどうするか?」という懸念もあったのですが、カメラもぐるぐるしながら孵化すれば脱出が容易であることを共有いただき、今回のコードの作成につなげることができました。(孵化中もカメラぐるぐるしているのは気になるかもですが…)
情報を共有いただきありがとうございます!
ただ、ダッシュ状態だと詰まりやすいこともあり、安定をとってダッシュはさせておりません。
ループ破綻について
手動でしか確認していないのですが、柵のポールにぶつかった際にごく稀に柵を飛び越えてしまうことを確認しています。
孵化中にはまだ確認できていないものの、発生する確率は0ではないと思われます。
ただ、灯台上とは異なり常に壁に密着しているわけではないことから発生率は低いと考えています。
最後に
今回のコードは不安要素が拭いきれていません。もし、それらの解決策など思いつくものがあれば@lefmarnaまで共有いただければと思います!
その他、ループから抜けてしまった場所があればそれも教えていただきたいです。ほとんどの場合は解決に至らないと思いますが、どんな例外が眠っているのか把握しておきたいと思っています!