ReactNativeで太⚫︎の達人を作ろうとした話

# はじめに
ReactNativeで太⚫︎の達人を作ろうとしたけど**スペックが足りなくてMACBookがフリーズした** ので、強いボードの自作PCでも買えるまで誰かに希望を託そうと思います。

　楽器音オブジェクトのプーリングにより連打に耐えられるようコーディングした方の検証もしたかったのですがスペックの問題で出来なかった為、簡単なスケルトンの紹介に留めます。

 :::note info
 この記事で多分役に立つこと
 1. 2025年3月4日時点でのReactNativeCLIを用いたアプリケーションの初期化法
 2. 音ゲームのアルゴリズム初歩スケルトン
 3. 簡単なおまけ(ウェルカムページをHelloworldにする)

:::
![-88pz8x.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4005781/26421bcd-d8f2-4a57-8c2f-09631076c419.jpeg)

# ReactNativeアプリの初期化
ReactNativeの初期化に最近まで使用されていたreact-native initコマンドは、2024年12月31日をもって非推奨となりました。

:::note info
今は、下記書式が最新の初期化コマンドです。(2025年3月4日時点)
```sh:
npx @react-native-community/cli init AppName
```
React Native 0.71以降はTypeScriptがデフォルトで含まれているため、**TypeScriptテンプレートを指定する必要はありません。** これで勝手にTypeScript仕様になります。逆にJavaScriptで書きたい人は明示的にJS仕様で初期化も可能なようです。
:::

恐らくReactNativeを使うくらいの人であればそもそもあまり基本的すぎる部分は重複するかと思いますので細かい操作は省略します。
私がAndroidユーザーなのもあり、今回はAndroidで紹介しますが、初期化時に最新のコマンドではcocoapodsを入れるかどうかも丁寧に聞かれるので楽になったかと思います。
音ゲーム位でしたらプラットフォーム固有のコンポーネントもそんなに大差ないのでクロスプラットフォーム開発もしやすいです。

# 音ゲームのアルゴリズム初歩スケルトン

:::note warn
 著作権的なとこが怖いので敢えて楽器のファイル名を変えたりしています。
察してください。。
:::

```text:階層構成
MusicApp/
│── android/              # Android用ネイティブコード（React Native CLIのみ）
│── ios/                  # iOS用ネイティブコード（React Native CLIのみ）
│── assets/               # 画像や音声ファイル
│   ├── images/
│   │   ├── piano.png
│   │   ├── drum.png
│   ├── sounds/
│   │   ├── piano.mp3
│   │   ├── drum.mp3
│── data               # 画像や音声ファイル
│   ├── notes.ts          # 譜面データ
│── src/                 
│   ├── components/      
│   │   ├── Note.tsx      # 譜面の音符
│   │   ├── DrumPad.tsx   # 叩くボタン
│   ├── utils/            
│   │   ├── sound.ts      # 音の再生
│   ├── screens
│   │   ├──GameScreen.tsx # ゲーム画面
│   ├── hooks/            # カスタムフック
│   │   ├── useSound.ts
│   ├── types/            # 型定義
│   │   ├── index.ts
│   ├── App.tsx           # メインのアプリケーションコンポーネント
│── index.js              # エントリーポイント（React Nativeが参照する）
│── tsconfig.json         # TypeScriptの設定ファイル
│── babel.config.js       # Babelの設定ファイル
│── package.json          # 依存関係管理
│── metro.config.js       # Metroバンドラーの設定
│── .gitignore            # Git管理外ファイル指定
│── README.md             # プロジェクトの説明
```

### 譜面データ
```typescript:src/data/notes.ts
export const notes = [
  { type: 'piano', time: 1000 },  // 1秒後
  { type: 'drum', time: 2000 },   // 2秒後
  { type: 'piano', time: 3000 },  // 3秒後
];

```

### 楽器を鳴らす
```typescript:src/utils/sound.ts
import Sound from 'react-native-sound';

Sound.setCategory('Playback');

export const piano = new Sound(require('../../assets/piano.mp3'), Sound.MAIN_BUNDLE);
export const drum = new Sound(require('../../assets/drum.mp3'), Sound.MAIN_BUNDLE);

export const playSound = (type: 'piano' | 'drum') => {
  if (type === 'piano') piano.play();
  if (type === 'drum') drum.play();
};
```

### 楽器をタップするボタン
```typescript:src/components/DrumPad.tsx
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
import { playSound } from '../utils/sound';

type DrumPadProps = {
  type: 'piano' | 'drum';
};

const DrumPad: React.FC<DrumPadProps> = ({ type }) => {
  return (
    <TouchableOpacity style={styles.button} onPress={() => playSound(type)}>
      <Text style={styles.text}>{type.toUpperCase()}</Text>
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  button: {
    padding: 20,
    margin: 10,
    backgroundColor: '#FF6347',
    borderRadius: 10,
    alignItems: 'center',
  },
  text: {
    color: 'white',
    fontSize: 18,
  },
});

export default DrumPad;

```

### ゲーム画面
```typescript:src/screens/GameScreen.tsx
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import DrumPad from '../components/DrumPad';
import { notes } from '../data/notes';

const GameScreen: React.FC = () => {
  const [time, setTime] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setTime((prev) => prev + 100);
    }, 100);
    return () => clearInterval(interval);
  }, []);

  return (
    <View style={styles.container}>
      {/* 譜面エリア */}
      <View style={styles.noteArea}>
        {notes.map((note, index) => (
          <Text key={index} style={styles.note}>
            {time >= note.time ? '🎵' : ' '}
          </Text>
        ))}
      </View>

      {/* ボタンエリア */}
      <View style={styles.buttonArea}>
        <DrumPad type="piano" />
        <DrumPad type="drum" />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#000',
  },
  noteArea: {
    flex: 2,
    width: '100%',
    justifyContent: 'center',
    alignItems: 'center',
  },
  note: {
    fontSize: 30,
    color: 'white',
  },
  buttonArea: {
    flexDirection: 'row',
    padding: 20,
  },
});

export default GameScreen;

```

### メインのアプリケーションコンポーネントを書き換える
```typescript:App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import GameScreen from './src/screens/GameScreen';

const Stack = createStackNavigator();

const App = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        <Stack.Screen name="Game" component={GameScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default App;

```

:::note warn
最初に長々と必要なモジュールをコツコツインストールすると面倒なので、
スケルトン作成がひと段落してから、`npm install`で必要なモジュールを一括でインストールしてビルドしてください。
ビルド時、同時にAndroidエミュレーターを起動しておいてください。
```sh:Metro bundlerを起動
AppROOT¥ npx react-native start
```
↑起動中にもう一つターミナルを開いて
```sh:
AppROOT¥ npx react-native run-android
```
を実行するとエミュレーターにウェルカムページが表示されます。
:::

# おまけ
ReactNative初期化直後にビルドすると、ウェルカムページが専用のリストなので、`初期のJavaScript時代のようにHelloworldになるテンプレート`を作りました。
初期化直後に**App.tsx** をこちらに差し替えるとシンプルになりますので、慣れている人は使いやすいと思います。
```typescript:App.tsx
import React from "react";
import { StyleSheet, Text, View } from "react-native";

interface AppProps {}

interface AppState {}

export default class App extends React.Component<AppProps, AppState> {
  render() {
    return (
      <View style={styles.container}>
        <Text>Hello, world!</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center"
  }
});
```