Как я создал визуальную новеллу для Android за 2 дня
На выходных я решил заняться новым проектом и воплотить в жизнь свою идею мобильной игры. Что особенно интересно, в качестве жанра игры я выбрал визуальную новеллу. В этой статье я поделюсь своим опытом создания игры за два дня, а также расскажу, как ChatGPT и Civitai помогли мне со сценарием и артами для игры.
React Native:
При разработке мобильной игры я решил использовать React Native. Этот выбор обусловлен моим увлечением работе с этим фреймворком. Несмотря на наличие множества альтернативных технологий, я отдал предпочтение React Native из-за его множества преимуществ и особенностей.
React Native позволяет создавать кроссплатформенные приложения, совместимые как с iOS, так и с Android. Это значительно экономит время и ресурсы, поскольку одна и та же база кода может использоваться для обеих платформ.
Основные принципы React Native очень похожи на принципы ReactJS. Он предназначен для разработки на JavaScript и TypeScript. Для начала работы с React Native требуются только базовые знания JavaScript и React.
Как написал сценарий при помощи ChatGPT:
Для создания сценария визуальной новеллы я взаимодействовал с ChatGPT в несколько этапов. Сначала я подал на вход идею создания визуальной новеллы и ее тему.
На этом этапе ChatGPT предоставил мне несколько дополнительных идей. Далее я начал писать различные запросы, связанные с тематикой игры, пытаясь получить примерный план сценария игры. И мне это удалось.
ChatGPT изложил план, похожий на тот, который мне был нужен. Каждый этап плана напоминал отдельные сцены игры. Однако мне все еще нужно было доработать эти сцены. Я кратко описал каждую сцену в ChatGPT. Я получил ответ, разбитый на кадры и диалоги. Если меня что-то не устраивало, я просил ChatGPT это исправить, уточнив, что именно не так и как должно быть. С первого раза не получилось, но после некоторого повторения мне удалось создать вполне приличный сценарий.
В определенный момент сценарий разветвился, что усложнило работу, поэтому мне пришлось перенести в Миро готовые сцены и кадры. Там создал диаграмму, чтобы визуализировать ветки сюжета и последовательность сцен.
Генерация картинок для игры при помощи нейросети:
Существует множество сервисов для генерации изображений с помощью нейронных сетей. Многие из них активно рекламируются и преподносятся как нечто крутое. Обычно они имеют веб-интерфейс, позволяющий создавать изображения прямо в браузере, ничего не устанавливая и не настраивая. Однако, попробовав некоторые из них, я был разочарован. Изображения не подходили для использования в игре. Как бы я ни составлял запрос на генерацию, мне не удалось найти ничего подходящего для мобильной визуальной новеллы, действие которой происходит в школе.
Пропуская аргументацию, просто посоветую использовать Stable Diffusion или NovelAI. Это все, что вам нужно.
Из-за нехватки времени и ограниченных ресурсов для изучения темы я воспользовался сайтом Civitai. Взяв одну из готовых пользовательских моделей, я сгенерировал для себя несколько артов, бесплатно, без СМС и прямо в браузере. Мне просто нужно было зарегистрироваться. Вот одно из изображений, которое я получил после генерации.
Написание кода для визуальной новеллы на ReactNative:
Как всегда ставим node если его нет.
brew install node
Установите Android Studio и Android SDK отсюда.
Установите переменную среды ANDROID_HOME.
Для разработки iOS вам необходимо установить Xcode.
Однако в этой статье мы опустим детали настройки среды разработки. Подробное руководство можно найти на сайте React Native. Начнем эту статью с создания и запуска проекта с помощью React Native CLI.
Создаем проект из шаблона:
npx react-native@latest init AwesomeProject
Запускаем его:
npx react-native start
Приступим к написанию кода визуальной новеллы. Создаем компонент BaseScene
, который будет обрабатывать логику отображения сцен в игре. Сценой будет считаться конкретная локация, где происходит действие в визуальном романе.
interface SceneProps {
backgroundImage: ImageProps['source'];
}
const BaseScene: React.FC<SceneProps> = ({
backgroundImage
}) => (
<View>
<Image
source={backgroundImage}
style={{width, height: '100%', resizeMode: 'cover'}}
/>
</View>
);
Всю сцену обернем в компонент Pressable
, чтобы нажатие на экран было тригером для открытие следующего кадра игры, диалога или следующей сцены.
interface SceneProps {
backgroundImage: ImageProps['source'];
onPress?(): void;
}
const BaseScene: React.FC<SceneProps> = ({
backgroundImage,
onPress
}) => (
<View>
<Pressable onPress={onPress} style={{flex: 1}}>
<Image
source={backgroundImage}
style={{width, height: '100%', resizeMode: 'cover'}}
/>
</Pressable>
</View>
);
Добавим отображение текста и автора текста.
interface SceneProps {
backgroundImage: ImageProps['source'];
onPress?(): void;
text?: string;
textAuthor?: string;
}
const BaseScene: React.FC<SceneProps> = ({
backgroundImage,
onPress,
text,
textAuthor
}) => (
<View
style={{
position: 'relative',
flex: 1,
}}>
<Pressable onPress={onPress} style={{flex: 1}}>
<Image
source={backgroundImage}
style={{width, height: '100%', resizeMode: 'cover'}}
/>
{text && (
<View
style={{
position: 'absolute',
bottom: 50,
backgroundColor: 'black',
padding: 8,
width: '100%',
}}>
{textAuthor && (
<View
style={{
position: 'absolute',
backgroundColor: 'black',
top: -36,
height: 36,
padding: 8,
borderTopRightRadius: 6,
}}>
<Text style={{color: 'white', fontSize: 16}}>{textAuthor}</Text>
</View>
)}
<Text style={{color: 'white', fontSize: 20}}>{text}</Text>
</View>
)}
</Pressable>
</View>
);
Далее опишем одну из игровых сцен.
Это будет Scene1
, или сцена в школьном коридоре.
Мы будем использовать компонент BaseScene
, описанный выше. Передадим ему изображение школьного коридора.
const Scene1 = () => {
return (
<BaseScene
backgroundImage={require('../assets/hallway.jpeg')}
/>
);
}
Давайте добавим контент в сцену. Мы передадим в BaseScene текст и изображение преподавателя, который будет этот текст говорить. Мы добавим изображение как children в BaseScene.
const Scene1 = () => {
const image = (
<Image
source={require('../assets/teacher.png')}
containerStyle={{
position: 'absolute',
bottom: 70,
}}
/>
);
const text = 'Hello';
const textAuthor = 'Teacher';
return (
<BaseScene
backgroundImage={require('../assets/hallway.jpeg')}
text={text}
textAuthor={textAuthor}
>
{image}
</BaseScrene>
);
}
В этой сцене будет более одного диалога и персонажа.
Давайте добавим объект под названием steps
, в котором будут храниться шаги — диалоги для этой сцены. Мы переместим изображение и текст в поля этого объекта. Также мы добавим в сцену еще один диалог.
enum Step {
first = 'first',
second = 'second'
}
const steps = {
[Step.first]: {
text: 'Class, we have a new student. Come on, introduce yourself, please',
textAuthor: 'Teacher',
children: (
<Image
source={require('../assets/teacher.png')}
containerStyle={{
position: 'absolute',
bottom: 70,
}}
/>
),
},
[Step.second]: {
text: 'Hello everyone, I'm {{name}}',
textAuthor: 'Hero',
children: (
<Image
source={require('../assets/teacher.png')}
containerStyle={{
position: 'absolute',
bottom: 70,
}}
/>
),
},
};
Давайте добавим состояние useState
. Он сохранит идентификатор текущего диалога, и мы добавим переходы между диалогами в сцене. Триггером перехода будет нажатие на экран.
const Scene1 = () => {
const [stepID, setStepID] = useState(Step.first);
const steps = {
[Step.first]: {
...
onPress: () => {
setStepID(Step.second);
},
},
...
};
const currentStep = steps[stepID];
return (
<BaseScene
backgroundImage={require('../assets/hallway.jpeg')}
{...currentStep}
/>
);
}
Некоторые шаги могут содержать вопросы к игроку. Давайте добавим возможность игроку вводить свое имя. Для этого добавим Step.third
, в котором будет модальное окно с компонентом Input
для ввода имени игрока.
...
const [name, setName] = useState('Hero');
...
const steps = {
...
[Step.third]: {
text: 'Enter your name...',
textAuthor: 'System',
children: (
<Modal animationType="slide" transparent={true} visible>
<View
style={{
...
}}>
<View
style={{
...
}}>
<Text style={{color: 'white', fontSize: 16}}>
{t('screen2.createName.title')}
</Text>
<TextInput
style={{
...
}}
placeholderTextColor="gray"
placeholder="Hero"
onChangeText={text => setName(text)}
/>
<Pressable
style={{
...
}}
onPress={() => {
setStepId(...);
}}>
<Text
style={{
...
}}
>Save</Text>
</Pressable>
</View>
</View>
</Modal>
)
}
}
Отлично, а что если пользователь закроет игру? Нам нужно сохранить состояние игры, чтобы мы могли продолжить с последнего сохранения. Для этого давайте добавим AsyncStorage
и сохраним в нем идентификатор текущего шага сцены, номер сцены и введенные пользователем данные (в настоящее время только имя игрока).
import AsyncStorage from '@react-native-async-storage/async-storage';
...
const saveStepID = (newStepID: Step) => {
const storeData = async (value: string) => {
try {
await AsyncStorage.setItem('stepID', value);
setStepID(value);
} catch (e) {
...
}
};
storeData(newScreen);
};
...
Далее нам нужно получить сохраненные данные при повторном открытии приложения. Давайте добавим useEffect
в компонент App
.
useEffect(() => {
const getData = async (itemName: string, setFunction: Function) => {
try {
const value = await AsyncStorage.getItem(itemName);
if (value !== null) {
setFunction(value as any);
}
} catch (e) {
// error reading value
}
};
getData('stepID', setStepID);
getData('sceneID', setSceneID);
...
}, []);
Давайте добавим музыку в игру. Мы будем использовать пакет response-native-sound
.
useEffect(() => {
Sound.setCategory('Playback');
const music = new Sound('school.mp3', Sound.MAIN_BUNDLE, error => {
if (error) {
console.log('failed to load the sound', error);
return;
}
musicLoadedRef.current = true;
music.setVolume(0.5);
const playMusic = () => {
music.play(playMusic);
};
playMusic();
});
return () => {
music.reset();
music.stop();
};
}, []);
Музыка должна перестать воспроизводиться, когда приложение находится в фоновом или неактивном состоянии. Для этого давайте подпишемся на изменения состояния приложения и соответственно приостановим и возобновим воспроизведение музыки.
import { AppState, ... } from 'react-native';
...
const appState = useRef(AppState.currentState);
useEffect(() => {
...
const subscription = AppState.addEventListener('change', nextAppState => {
if (appState.current === 'active' && nextAppState !== 'active') {
music.stop();
} else if (
appState.current !== 'active' &&
nextAppState === 'active' &&
musicLoadedRef.current
) {
const playMusic = () => {
music.play(playMusic);
};
playMusic();
}
appState.current = nextAppState;
});
...
}, [])
...
Затем я добавил локализацию на другие языки с помощью react-i18next
. Я добавил больше сцен, шагов внутри сцен с возможностью выбора различных вариантов развития сюжета. Я реализовал анимированные переходы между сценами и шаги внутри сцен с помощью Animated
. Я добавил звуковые эффекты шагов, стука дверей для более глубокого погружения в игру. Я включил в игру вступительный и конечный экран и предоставил возможность оценить игру в Google Play.
Если вас интересует более подробное описание того, как я это сделал, напишите мне в социальных сетях и я опубликую более подробное описание процесса создания игры.
Поиск музыки для игры:
Основную музыкальную тему игры я создал с помощью приложения GarageBand.
Звуки открытия дверей, шагов, стука в дверь и шума школы я нашел на сайте freesound. Главное при поиске музыки и звуков обратите внимание на тип лицензии, чтобы он позволял использовать ее для ваших нужд. Например лицнезия Creative Commons Zero (CC0) позволяет использовать материал в общественном достоянии без ограничений.
Об игре:
Игра доступна для скачивания в Google Play по этой ссылке. Это игра в жанре визуальная новелла и названиется она "Один шаг в новой школе". В ней мы начинаем наш путь как ученик недавно переведенный в новую школу. Мы познакомимся со своим учителем и однокласниками. Сходим с ними в школьный буфет и хорошо проведем время. В этой игре есть музыка, озвучка основных действий, например шагов по коридору или стука в дверь для более глубокого погружения в игру. Весь сценарий игры написан при помощи нейросети. Попробуйте поиграть и напишите ваше мнение об этой игре.