EDITOR From Order to Pickup: Digital Tools That Save Your Festival Sales Flow
1 minutes
Hello everyone, the dev.family team is back. We continue to explore the integration of a Unity project into an application written with React Native.
Previously on dev.family chronicles…
We started by adding a Unity game to our app. What it was like? You can read on our blog. Right now we have parts of the code that don't interact with each other. It means the work's not over. And there is the «Save Result» button. It would be nice to write logic for it to show what we have.
Spoiler: that and more is what we're going to do right now.
What happens next
In the second part of our article, we'll take the current React Native + Unity bundle and make it able to receive and process messages from one part to another. And vice versa.
Let’s continue our Unity-journey!
Warning
The article is aimed to show how processes are working on a test application. It is neither a panacea nor a clear guidance of exactly how code should be written, but the solution that has worked out great for us, so we’d love to inspire you to create similar development challenges.
First, let’s start with React Native code.
Take a look on what we’ve added:
Next we pass our unityRef in UnityView and call for handleUnityMessage in UnityView too.
To avoid unnecessary syntax highlighting, we’ll add:
After that, we’re adding the list with our results to HomeScreen, where we’ll record the 10 best tries in the game.
Then we go to Unity Editor.
Before we set up receiving messages, let's add a new field that will display our best score.
Here we’ll add two text fields:
How it looks on the screen:
Now let’s uncover what we wanna get out from message transmission to/from Unity:
But before we need to understand how to send and process messages with Unity.
First, let's add a script and call itMessageToReactScript. Here we’ll insert the following code. You can also find it on the library’s GitHub page:
This script will help us to transmit messages from Unity.
Now let’s modify it a little bit to get the result we want:
Please note: the example demonstrates sending only one string. Therefore, we'll transmit everything in JSON format to streamline the process and avoid unnecessary complications.
After that, let’s add a new class describing our record. It has two fields – date and score. Current results with its date and time are written directly in the score. All you need to do next is to stringify the score instance in JSON and send it to the mobile app.
Our script is ready. Now we create the new GameObject ReactBridge:
Insert our script in it:
Go to Canvas and find the «Save Result» button that we’ve made the last time. Insert our ButtonPressed method from the snippet described above into it. Firstly, add the GameObjectReactBridge, select the MessageToReactScript script, and then theButtonPressed()method itself:
When we click the «Save Result» button, we send JSON with our result to the mobile app.
The basic preparation for transmitting a message from Unity is complete. Now we need to compile and reinstall the Unity build on Android and iOS.
Here you can use the files from the library repository. Just follow the link.
Let’s get synchronized. At the moment we have:
How to rebuild the project and insert it into a mobile app, we've already covered in the first part of the article.
Now let’s start with processing the message. First, we’ll set an alert to make sure our message is actually transmitted when the button is clicked.
Doing everything as above, we get the following result:
What do we see? The message appeared. It shows the details of the score and the time it was received. Now let’s put it in our scores state. But if we don’t want to lose the data let’s put in async-storage. But firstly, install the library itself.
For this enter the following commands:
It's possible that someone might want to use faster storage like mmkv, but it’s not critical. So async-storage will be enough for us.
After that, we’ll come back to React Native code. Go to the Home Screen and write the new value in the list.
Let’s rewrite the message processing in Unity this way:
Here we parse our JSON with the result and then go to HomeScreen, throwing the result in parameters
We can also add this change to our navigation types (it will help us to avoid errors from typescript):
After that, we modifyHomeScreen, to make it record new data instorageandstatewhile it receives the result through navigation. Please note: in our case we only store the top 10 results in descending order. You can store and show as many as you need in any order you like.
That's what we got:
We also made some UI changes: added date processing to avoid ISO data format (doesn't look very good, don't you think?) and slightly changed the styles. You can do the same or change the interface however you like.
Here is the final result:
Now you can rebuild it for Android and check that everything is working the way it should:
Most of our functionality is ready to go, so we can safely transmit the data from Unity to React Native. Now it’s time to work with one more important task – organize data transfer from React Native to Unity.
Surprise-surprise! It turned out that we have already had a block of code that was ready for sending messages using React Native:
Here we must check whether the navigation parameters contain messageToUnity. If there is one, call postMessage function. As we can see from the description it can take in the following arguments:
Therefore, we need to paste here our game object, specify its function and message.
Let’s use the LogicManager object for this and add a new function to process this message.
Then we'll switch to Unity and make the final changes: add a new line with BestScore to the LogicManagerScript, and then create the function itself that will change its value:
Here we’ve added:
After that, in Unity Editor put the text element to LogicManagerScript (which is located inside GameObject LogicManager ), to make the value of our BestScoreText changeable.
And this is where our work in Unity ends. But you can always add something else or improve the written code.
The only thing you should do is to rebuild again.
Come back to React Native. First, we change our switching to the Unity page function, so that we can take the best results and send them to the game, where we have them.
Add the following function to HomeScreen:
After that, our Unity screen will always wait for the message. Then we put this function to onPress of our «Go Unity» button:
Add the values to the function for posting messages to Unity. On UnityScreen change or add the following part:
In that case, we specify that the gameObject and methodName to which we transmit our message are LogicManager and SetBestScore, respectively. At the same time, our message stores the best result posted by the HomeScreen (our message is a line that can store not only text but also JSON objects).
Let's run the apps on iOS and Android and see if everything works:
iOS:
Android:
Our two in-depth articles uncover the integration of a Unity project into an application written with React Native. So here's a step-by-step guidance we've been working with ourselves. We hope that our experience will make it easier to solve similar problems or inspire you on new projects. You can also learn a bit more about the process by reading about plugins in Unity.
All code examples can be found at the links below.
It was the dev.family team on line, and see you later ;)
const UnityScreen: React.FC<RootStackScreenProps<RootRoutes.UNITY>> = ({
route,
}) => {
// Start
const unityRef = useRef<UnityView>(null);
const {messageToUnity} = route.params;
useEffect(() => {
if (messageToUnity) {
unityRef.current?.postMessage('', '', messageToUnity);
}
}, [messageToUnity]);
const handleUnityMessage = (message: string) => {
console.log(message);
};
//End
return (
<UnityView
ref={unityRef}
//@ts-expect-error UnityView needs a 'flex: 1' style to show full screen view
style={styles.flex}
onUnityMessage={e => handleUnityMessage(e.nativeEvent.message)} // and this line
/>
);
};type RootStackParamList = {
[RootRoutes.HOME]: undefined;
[RootRoutes.UNITY]: {messageToUnity?: string}; // added messageToUnity
};// score data type
type Score = {
date: string;
score: number;
};
const HomeScreen: React.FC<RootStackScreenProps<RootRoutes.HOME>> = ({
navigation,
}) => {
const [scores, setScores] = useState<Score[]>([]); // scores to display in list
const insets = useSafeAreaInsets();
//List item to render
const renderScore: ListRenderItem<Score> = useCallback(({item, index}) => {
return (
<View style={styles.score}>
<Text style={styles.scoreText}>{index + 1}.</Text>
<Text style={[styles.scoreText, styles.flex]}>{item.score}</Text>
<Text style={styles.scoreDate}>{item.date}</Text>
</View>
);
}, []);
return (
<View style={[styles.screen, {paddingBottom: Math.max(insets.bottom, 15)}]}>
<Text style={styles.welcomeText}>
Hello, from{' '}
<Text style={[styles.welcomeText, styles.purple]}>dev.family</Text> team
</Text>
{/** scoreboard */}
<Text style={styles.welcomeText}>Scores 🏆:</Text>
<FlatList
data={scores}
renderItem={renderScore}
keyExtractor={i => i.date}
style={styles.list}
contentContainerStyle={styles.listContent}
ListEmptyComponent={<Text>You have no scoreboard yet</Text>}
/>
<TouchableOpacity
style={styles.button}
onPress={() => {
navigation.navigate(RootRoutes.UNITY, {messageToUnity: ''});
}}>
<Text style={styles.buttonText}>Go Unity</Text>
</TouchableOpacity>
</View>
);
};using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine.UI;
using UnityEngine;
public class NativeAPI {
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal")]
public static extern void sendMessageToMobileApp(string message);
#endif
}
public class ButtonBehavior : MonoBehaviour
{
public void ButtonPressed()
{
if (Application.platform == RuntimePlatform.Android)
{
using (AndroidJavaClass jc = new AndroidJavaClass("com.azesmwayreactnativeunity.ReactNativeUnityViewManager"))
{
jc.CallStatic("sendMessageToMobileApp", "The button has been tapped!");
}
}
else if (Application.platform == RuntimePlatform.IPhonePlayer)
{
#if UNITY_IOS && !UNITY_EDITOR
NativeAPI.sendMessageToMobileApp("The button has been tapped!");
#endif
}
}
}using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine.UI;
using UnityEngine;
public class NativeAPI
{
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal")]
public static extern void sendMessageToMobileApp(string message);
#endif
}
//Score class
public class Score
{
public int score;
public string date;
public Score(int score, string date)
{
this.score = score;
this.date = date;
}
}
public class MessageToReactScript : MonoBehaviour
{
private Text _score;
public void ButtonPressed()
{
//getting current date in ISO format
var date = DateTime.Now.ToString("o");
//getting current score from UI
_score = GameObject.FindGameObjectWithTag("Score").GetComponent<Text>();
//creating an instance of Score class
Score score = new(int.Parse(_score.text), date);
//pare score object to json (we sending a string)
string scoreJSON = JsonUtility.ToJson(score);
print(scoreJSON);
if (Application.platform == RuntimePlatform.Android)
{
using (AndroidJavaClass jc = new AndroidJavaClass("com.azesmwayreactnativeunity.ReactNativeUnityViewManager"))
{
//send message to android
jc.CallStatic("sendMessageToMobileApp", scoreJSON);
}
}
else if (Application.platform == RuntimePlatform.IPhonePlayer)
{
#if UNITY_IOS && !UNITY_EDITOR
//send message to iOS
NativeAPI.sendMessageToMobileApp(scoreJSON);
#endif
}
}
}const handleUnityMessage = (message: string) => {
//alert to show Unity message data
alert(message);
const score = JSON.parse(message) as Score;
console.log({score});
};yarn add @react-native-async-storage/async-storage
npx pod-install или cd ios && pod install && cd ..const handleUnityMessage = (message: string) => {
//alert to show Unity message data
const score = JSON.parse(message) as Score; //parse message to Score Object
if (score) {
unityRef.current?.unloadUnity();//unload Unity View
navigation.navigate(RootRoutes.HOME, {score}); //going to Home Screen with recent score
}
};type RootStackParamList = {
[RootRoutes.HOME]: {score?: Score}; // can get Score from Unity Screen
[RootRoutes.UNITY]: {messageToUnity?: string}; // added messageToUnity
};//func to setup scores from async storage on app open (we have no scores)
const setupScores = async () => {
const scoresJSON = await AsyncStorage.getItem('scores');
if (scoresJSON) {
setScores(JSON.parse(scoresJSON) as Score[]);
}
};
//setting up existed scores
useEffect(() => {
if (!scores.length) {
setupScores();
}
});
const setNewScores = async (score: Score) => {
//creating new scores with new one, includes filter & sort to show only 10 best results
const newScores = [...scores, score]
.sort((a, b) => b.score - a.score)
.slice(0, 10);
//setting new scores to async storage
await AsyncStorage.setItem('scores', JSON.stringify(newScores));
//setting new scores to scores' state
setScores(newScores);
//clean navigation score param
navigation.setParams({score: undefined});
};
useEffect(() => {
if (route.params?.score) {
setNewScores(route.params.score);
}
}, [route.params]);const {messageToUnity} = route.params;
useEffect(() => {
if (messageToUnity) {
unityRef.current?.postMessage('', '', messageToUnity);// right here
}
}, [messageToUnity]);(gameObject: string, methodName: string, message: string)using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using TMPro;
public class LogicManagerScript : MonoBehaviour
{
private int _score;
[SerializeField]
private Text _scoreText;
[SerializeField]
private GameObject _gameOverScreen;
//best score field where we write value from RN part
public TextMeshProUGUI bestScoreText;
[SerializeField]
private GameObject _startScreen;
[SerializeField]
private GameObject _game;
[ContextMenu("Increase Score")]
public void IncreaseScore(int number)
{
_score += number;
_scoreText.text = _score.ToString();
}
//rewrite bestScoreText value with RN message
public void SetBestScore(string message)
{
bestScoreText.text = message;
}
public void RestartGame()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
public void GameOver()
{
_gameOverScreen.SetActive(true);
}
public void StartGame()
{
_startScreen.SetActive(false);
_game.SetActive(true);
}
}const goUnity = () => {
let messageToUnity = '0';// set default value to 0
if (scores.length) {
// if we have scores select max value
// element with 0 index is the highest because we’ve sorted our scores
messageToUnity = scores[0].score.toString();
}
//go to Unity with max score = messageToUnity
navigation.navigate(RootRoutes.UNITY, {messageToUnity});
};<TouchableOpacity style={styles.button} onPress={goUnity}>//added a method
<Text style={styles.buttonText}>Go Unity</Text>
</TouchableOpacity>const {messageToUnity} = route.params; // getting our message from route params
//creating message object (not necessary)
const message = {
gameObject: 'LogicManager',
method: 'SetBestScore',
message: messageToUnity,
};
//on getting message from route posting it to Unity
useEffect(() => {
if (messageToUnity) {
unityRef.current?.postMessage(
message.gameObject,
message.method,
message.message,
);
}
}, [messageToUnity]);import React, {useCallback, useEffect, useRef, useState} from 'react';
import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {
FlatList,
ListRenderItem,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import {
SafeAreaProvider,
useSafeAreaInsets,
} from 'react-native-safe-area-context';
import UnityView from '@azesmway/react-native-unity';
import AsyncStorage from '@react-native-async-storage/async-storage';
import type {NativeStackScreenProps} from '@react-navigation/native-stack';
enum RootRoutes {
HOME = 'Home',
UNITY = 'Unity',
}
type RootStackParamList = {
[RootRoutes.HOME]: {score?: Score};
[RootRoutes.UNITY]: {messageToUnity: string}; // added messageToUnity
};
type RootStackScreenProps<RN extends keyof RootStackParamList = RootRoutes> =
NativeStackScreenProps<RootStackParamList, RN>;
const Stack = createNativeStackNavigator<RootStackParamList>();
// score data type
type Score = {
date: string;
score: number;
};
const HomeScreen: React.FC<RootStackScreenProps<RootRoutes.HOME>> = ({
navigation,
route,
}) => {
const [scores, setScores] = useState<Score[]>([]); // scores to display in list
const insets = useSafeAreaInsets();
//func to setup scores from async storage on app open (we have no scores)
const setupScores = async () => {
const scoresJSON = await AsyncStorage.getItem('scores');
if (scoresJSON) {
setScores(JSON.parse(scoresJSON) as Score[]);
}
};
//setting up existed scores
useEffect(() => {
if (!scores.length) {
setupScores();
}
});
const setNewScores = async (score: Score) => {
//creating new scores with new one, includes filter & sort to show only 10 best results
const newScores = [...scores, score]
.sort((a, b) => b.score - a.score)
.slice(0, 10);
//setting new scores to async storage
await AsyncStorage.setItem('scores', JSON.stringify(newScores));
//setting new scores to scores' state
setScores(newScores);
//clean navigation score param
navigation.setParams({score: undefined});
};
useEffect(() => {
if (route.params?.score) {
setNewScores(route.params.score);
}
}, [route.params]);
const goUnity = () => {
let messageToUnity = '0';
if (scores.length) {
messageToUnity = scores[0].score.toString();
}
navigation.navigate(RootRoutes.UNITY, {messageToUnity});
};
//List item to render
const renderScore: ListRenderItem<Score> = useCallback(({item, index}) => {
return (
<View style={styles.score}>
<Text style={styles.scoreText}>{index + 1}.</Text>
<Text style={[styles.scoreText, styles.flex]}>{item.score}</Text>
<Text style={styles.scoreDate}>
{new Date(item.date).toLocaleString()}
</Text>
</View>
);
}, []);
return (
<View style={[styles.screen, {paddingBottom: Math.max(insets.bottom, 15)}]}>
<Text style={styles.welcomeText}>
Hello, from{' '}
<Text style={[styles.welcomeText, styles.purple]}>dev.family</Text> team
</Text>
{/** scoreboard */}
<Text style={styles.welcomeText}>Scores 🏆:</Text>
{!!scores.length && (
<View style={[styles.row, styles.scoreInfo]}>
<Text style={[styles.scoreText, styles.flex]}>Score</Text>
<Text style={styles.scoreText}>Date</Text>
</View>
)}
<FlatList
data={scores}
renderItem={renderScore}
keyExtractor={i => i.date}
style={styles.list}
contentContainerStyle={styles.listContent}
ListEmptyComponent={<Text>You have no scoreboard yet</Text>}
/>
<TouchableOpacity style={styles.button} onPress={goUnity}>
<Text style={styles.buttonText}>Go Unity</Text>
</TouchableOpacity>
</View>
);
};
const UnityScreen: React.FC<RootStackScreenProps<RootRoutes.UNITY>> = ({
route,
navigation,
}) => {
// Start
const unityRef = useRef<UnityView>(null);
const {messageToUnity} = route.params;
const message = {
gameObject: 'LogicManager',
method: 'SetBestScore',
message: messageToUnity,
};
useEffect(() => {
if (messageToUnity) {
unityRef.current?.postMessage(
message.gameObject,
message.method,
message.message,
);
}
}, [messageToUnity]);
const handleUnityMessage = (json: string) => {
//alert to show Unity message data
const score = JSON.parse(json) as Score;
if (score) {
// unityRef.current?.unloadUnity();
navigation.navigate(RootRoutes.HOME, {score});
unityRef.current?.unloadUnity();
}
};
//End
return (
<View style={styles.flex}>
<UnityView
ref={unityRef}
//@ts-expect-error UnityView needs a 'flex: 1' style to show full screen view
style={styles.flex}
onUnityMessage={e => handleUnityMessage(e.nativeEvent.message)} // and this line
/>
</View>
);
};
const App = () => {
return (
<View style={styles.flex}>
<StatusBar backgroundColor={'#FFF'} barStyle="dark-content" />
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerTintColor: 'purple',
headerTitleStyle: {color: 'black'},
}}>
<Stack.Screen name={RootRoutes.HOME} component={HomeScreen} />
<Stack.Screen name={RootRoutes.UNITY} component={UnityScreen} />
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
</View>
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
paddingHorizontal: 16,
gap: 30,
paddingTop: 25,
},
button: {
width: '100%',
backgroundColor: 'purple',
justifyContent: 'center',
alignItems: 'center',
height: 50,
borderRadius: 16,
marginTop: 'auto',
},
purple: {color: 'purple'},
buttonText: {
color: '#FFF',
fontSize: 16,
fontWeight: '600',
},
welcomeText: {
fontSize: 24,
color: 'black',
fontWeight: '600',
},
flex: {
flex: 1,
},
row: {flexDirection: 'row'},
scoreInfo: {paddingHorizontal: 14},
score: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingBottom: 6,
borderBottomWidth: 1,
borderColor: '#bcbcbc',
},
scoreText: {
fontSize: 18,
fontWeight: '500',
color: 'black',
},
scoreDate: {
color: '#262626',
fontSize: 16,
fontWeight: '400',
},
list: {
flex: 1,
},
listContent: {
flexGrow: 1,
paddingBottom: 20,
gap: 12,
},
});
export default App;




