我如何在 2 天内使用 React Native 制作视觉小说
在周末,我决定开始一个新项目,并将我的移动游戏想法付诸实践。最重要的是,我选择了视觉小说作为游戏类型。在本文中,我将分享我在两天内创建游戏的经验,并讨论 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 进行更正,具体说明哪里出了问题以及应该如何。虽然第一次尝试没有成功,但经过一些重复,我成功创建了一个相当不错的脚本。
在某个时刻,脚本分支,使得工作变得困难,因此我不得不将完成的场景和帧转移到 Miro。在那里,我创建了一个图表,以便可视化情节分支和场景的顺序。
使用神经网络生成游戏图像:
有许多服务可以使用神经网络生成图像。许多服务被积极宣传并被呈现为很酷的东西。通常,它们有一个 Web 界面,允许您直接在浏览器中创建图像,而无需安装或配置任何东西。然而,尝试了一些之后,我感到失望。这些图像不适合用于游戏。无论我如何构建生成查询,我都找不到适合在学校设置的移动视觉小说的东西。
跳过论证,我只建议您使用 Stable Diffusion 或 NovelAI。这就是您所需要的。
由于时间限制和探索主题的资源有限,我使用了 Civitai 网站。使用其中一个现成的用户模型,我为自己生成了一些艺术作品,免费,无需短信,直接在浏览器中。我只需注册即可。以下是我生成后获得的图像之一。
在 React Native 中为视觉小说编写代码:
一如既往,确保安装了 Node.js。如果没有,请安装:
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('../images/hallway.jpeg')}
/>
);
}
让我们为场景添加内容。我们将传递文本和教师的图像给 BaseScene,他将说出这个文本。 我们将图像作为子元素添加到 BaseScene。
const Scene1 = () => {
const image = (
<Image
source={require('../images/teacher.png')}
containerStyle={{
position: 'absolute',
bottom: 70,
}}
/>
);
const text = '你好';
const textAuthor = '老师';
return (
<BaseScene
backgroundImage={require('../images/hallway.jpeg')}
text={text}
textAuthor={textAuthor}
>
{image}
</BaseScrene>
);
}
在场景中,有不止一个对话和角色参与。
让我们添加一个名为 steps
的对象,它将存储此场景的步骤 - 对话。我们将图像和文本移动到此对象的字段中。此外,我们将在场景中添加另一个对话。
enum Step {
first = 'first',
second = 'second'
}
const steps = {
[Step.first]: {
text: '班级,我们有一个新学生。来吧,请介绍一下自己',
textAuthor: '老师',
children: (
<Image
source={require('../images/teacher.png')}
containerStyle={{
position: 'absolute',
bottom: 70,
}}
/>
),
},
[Step.second]: {
text: '大家好,我是{{name}}',
textAuthor: '主角',
children: (
<Image
source={require('../images/teacher.png')}
containerStyle={{
position: 'absolute',
bottom: 70,
}}
/>
),
},
};
让我们添加 useState
状态。它将存储当前对话 ID,并且我们将在场景中添加对话之间的过渡。过渡的触发器将是屏幕上的按压。
const Scene1 = () => {
const [stepID, setStepID] = useState(Step.first);
const steps = {
[Step.first]: {
...
onPress: () => {
setStepID(Step.second);
},
},
...
};
const currentStep = steps[stepID];
return (
<BaseScene
backgroundImage={require('../images/hallway.jpeg')}
{...currentStep}
/>
);
}
某些步骤可能包含玩家的问题。让我们添加玩家输入自己名字的能力。为此,我们将添加 Step.third
,其中将有一个模态窗口,带有一个 Input
组件,用于输入玩家的名字。
...
const [name, setName] = useState('主角');
...
const steps = {
...
[Step.third]: {
text: '输入你的名字...',
textAuthor: '系统',
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="主角"
onChangeText={text => setName(text)}
/>
<Pressable
style={{
...
}}
onPress={() => {
setStepId(...);
}}>
<Text
style={{
...
}}
>保存</Text>
</Pressable>
</View>
</View>
</Modal>
)
}
}
很好,但如果用户关闭游戏怎么办?我们需要保存游戏状态,以便可以从上次保存的地方继续。为此,让我们添加 AsyncStorage
并将当前场景步骤 ID、场景编号和用户输入(目前仅为玩家的名字)保存到其中。
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);
};
...
接下来,我们需要在重新打开应用程序时检索保存的数据。让我们在 App
组件中添加 useEffect
。
useEffect(() => {
const getData = async (itemName: string, setFunction: Function) => {
try {
const value = await AsyncStorage.getItem(itemName);
if (value !== null) {
setFunction(value as any);
}
} catch (e) {
// 读取值时出错
}
};
getData('stepID', setStepID);
getData('sceneID', setSceneID);
...
}, []);
让我们为游戏添加音乐。我们将使用 react-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 上下载。 这是一款名为 "One Step in high School" 的视觉小说游戏。 在其中,我们将作为一名新转学到新学校的学生开始我们的旅程。 我们将认识我们的老师和同学,与他们一起去学校食堂,并度过美好时光。此游戏包含音乐和关键动作的音效,例如走廊中的脚步声或敲门声,以便更深入地沉浸在游戏中。整个游戏脚本是使用神经网络编写的。试试看,并分享您对这款游戏的反馈。