Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9409

React NativeでSocket通信しよう

$
0
0
(1)はじめに 普段はソーシャルワーカーとして障がいをお持ちの方の支援をしています。Advent Calendarに合わせてバージョンアップさせているReact Nativeでの3D表示について、続きを書いていきたいと思います。前回はReact Nativeで3Dキャラクターを動かしてみようで3Dキャラクターをバーチャルスティックで動かすコードを紹介しました。今回はチャットやマルチプレイで利用するSocket通信の機能を追加したいと思います。 赤色のキャラクターは自身の端末、青色のキャラクターは別の端末で操作しています。 (2)Socket通信のためのサーバー まずはnodejsでサーバーを作成します。基本的にはアプリから受け取ったデータを配信するだけの機能です。フレームワークはexpressを利用し、端末から送られてきたデータをsocket.onで取得、socket.broadcast.emitで送信してきた端末以外に配信します。 const express = require("express"); const cors = require("cors"); const http = require("http"); const socketIO = require("socket.io"); require("events").EventEmitter.defaultMaxListeners = 0; const port = process.env.PORT || 3000; const app = express(); const server = http.createServer(app); const io = socketIO(server, { cors: true, origins: ["*"], methods: ["GET", "POST"], credentials: true, }); app.use(cors()); app.use(express.urlencoded({ extended: true, limit: "10mb" })); app.use(express.json({ extended: true, limit: "10mb" })); io.on("connection", (socket) => { socket.on("FromAPI", (data) => { socket.broadcast.emit("FromAPI", data); socket.on("disconnect", () => { console.log("クライアント接続が切れました"); }); }); }); server.listen(port, () => { console.log(`listening on *:${port}`); }); サーバーですが、herokuだとラグが気になったのでさくらvpnに立てました。KUSANAGIというOSを使うと一瞬で秘密鍵やssl設定ができます。月800円程の定額なので、テスト用にひとつ立てておくと便利です。また、"(node) warning: possible EventEmitter memory leak detected. 11 listeners added. Use emitter.setMaxListeners() to increase limit."がログ表示されるのでrequire("events").EventEmitter.defaultMaxListeners = 0;で対応しています。 (3)使った技術 ・react-native ・expo 42.0.1 ・expo-gl 10.4.2 ・expo-three 6.0.1 ・gsap 3.6.0 ・three 0.132.0 ← 今回のコードはバージョンによってはエラーになります ・base-64 1.0.0 ・expo-asset 8.3.3 ・socket.io-client 4.4.0 ← Socket通信用に追加 別端末の相手の3Dキャラクターを追加、表示させます。 参考にしたサイト Three.js公式 Expo公式 GLView Mixamo公式 Socket.io公式 React Native(Expo)のSocket通信についてはあまり情報がなく、実機でのテスト環境では問題なく動作しましたが、ストアにリリースした場合の動作は検証できていませんのでご了承ください。 (4)3Dモデル表示 バーチャルスティックは前回同様です。今回は相手の3Dモデルを追加でMixamoで作成し読み込ませます。設定するanimationは自身の3Dモデルと同様になります。自身のモデルをmodelsA 相手をmodelsBとしました。socketについてはuseRefを使い参照しています。このままではログ( gl.pixelStorei() doesn't support this parameter yet! from TextureLoader) が大量に出ますのでnode_modulesにある/three/build/three.js 16527行目からの下記3行をコメントアウトしておきます。※github expo-threeのissue#196参照 // _gl.pixelStorei(_gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha); // _gl.pixelStorei(_gl.UNPACK_ALIGNMENT, texture.unpackAlignment); // _gl.pixelStorei(_gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, _gl.NONE); できる限りそのままのコードにコメントしました。 //App.js import React, { useState, useEffect, useRef } from "react"; import { View } from "react-native"; import { GLView } from "expo-gl"; import { Renderer } from "expo-three"; import { TweenMax } from "gsap"; import { PointLight, GridHelper, PerspectiveCamera, Scene, AnimationMixer, // アニメーションのため追加 Clock, // アニメーションのため追加 } from "three"; import Positon from "./Position"; // バーチャルスティックのjs import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; import { Asset } from "expo-asset"; // ファイル読み込みのため追加 import io from "socket.io-client"; // クライアント用Socket.io export default function App() { const [cameras, setCameras] = useState(null); const [modelsA, setModelsA] = useState(null); // 自身の3Dモデルをセットする変数 const [walkA, setWalkA] = useState(true); // 自身のアニメーションをセットする変数 const [modelsB, setModelsB] = useState(null); // 相手の3Dモデルをセットする変数 const [walkB, setWalkB] = useState(true); // 相手のアニメーションをセットする変数 const [action, setAction] = useState({ z: 0, x: 0 }); // 自身のキャラクターの座標をセットする変数 const socketRef = useRef(); useEffect(() => { // サーバーのアドレス const socket = io("<サーバーのアドレス>"); socket.on("connect", () => { // 接続されたときにFromAPIから座標と回転、歩いているか否かの値を受け取る socket.on("FromAPI", (data) => { // 相手のキャラクターに値をセットする modelsB.position.set(data.x, 0, data.z); modelsB.rotation.y = data.y; walkB.paused = data.w; walkB.play(); }); }); socket.on("disconnect", () => { console.log("接続が切れました"); }); socket.on("connect", () => { console.log("接続されました"); }); socketRef.current = socket; return () => socket.disconnect(); }, [modelsB]); // FromAPIと名付けて自身のキャラクターの座標と回転、歩いているか否かの値をサーバーに送る const send = (props) => { socketRef.current.emit("FromAPI", { x: props.x, y: props.y, z: props.z, w: props.w, }); }; const walk = () => { // 自身のキャラクターとカメラの視点を同時に座標移動させて三人称視点にする   // TweenMax.to([何と、何が], { z軸にaction分移動 }) TweenMax.set([modelsA.position,cameras.position], { z: `+= ${action.z}`, x: `+= ${action.x}`, }); // Math.atan2で算出したradianに1.5を加算し前後左右にいい感じで向くようにする modelsA.rotation.y = Math.atan2(-action.z, action.x) + 1.5; // サーバーに自身のキャラクターの座標と回転、歩いているか否かの値を関数sendに渡す send({ x: modelsA.position.x, y: modelsA.rotation.y, z: modelsA.position.z, w: walkA.paused, }); }; const move = (props) => { walkA.paused = false; // キャラクターのポーズを解除 walkA.play(); // // アニメーションである変数walkを再生 setAction({ z: props.y, x: props.x }); // Position.jsから受け取った座標を変数actionにセット walk(); // walk関数を実行 }; // Position.jsから画面から指を離すことで発火する const end = () => { // アニメーションをストップ walkA.paused = true; // ストップした時の自身のキャラクターの座標と回転、歩いているか否かの値をsend関数に渡す send({ x: modelsA.position.x, y: modelsA.rotation.y, z: modelsA.position.z, w: walkA.paused, }); }; return ( <> <View style={{ flex: 1 }}> <GLView style={{ flex: 1 }} onContextCreate={async (gl) => { // 3D空間の準備 const { drawingBufferWidth: width, drawingBufferHeight: height } = gl; const renderer = new Renderer({ gl }); // レンダラーの準備 renderer.setSize(width, height); // 3D空間の幅と高さ renderer.setClearColor("white"); // 3D空間の配色 const scene = new Scene(); // これが3D空間 scene.add(new GridHelper(100, 100)); //グリッドを表示 // GLTFをロードする const loader = new GLTFLoader(); // 自身のキャラクターを設置 const assetA = Asset.fromModule(require("./assets/testA.glb")); await assetA.downloadAsync(); let mixerA; let clockA = new Clock(); loader.load( assetA.uri || "", (gltf) => { const modelA = gltf.scene; modelA.position.set(0, 0, 0); // 配置される座標 (x,y,z) modelA.rotation.y = Math.PI; const animations = gltf.animations; //Animation Mixerインスタンスを生成 mixerA = new AnimationMixer(modelA); // 設定した一つ目のアニメーションを設定 let animation = animations[0]; // アニメーションを変数walkにセット setWalkA(mixerA.clipAction(animation)); // testA.glbを3D空間に追加 scene.add(modelA); setModelsA(modelA); }, (xhr) => { console.log("ロード中"); }, (error) => { console.error("読み込めませんでした"); } ); // 相手のキャラクターを設置 let mixerB; let clockB = new Clock(); const assetB = Asset.fromModule(require("./assets/testB.glb")); await assetB.downloadAsync(); loader.load( assetB.uri || "", (gltf) => { const modelB = gltf.scene; modelB.position.set(0, 0, 0); // 配置される座標 (x,y,z) modelB.rotation.y = Math.PI; const animations = gltf.animations; //Animation Mixerインスタンスを生成 mixerB = new AnimationMixer(modelB); // 設定した一つ目のアニメーションを設定 let animation = animations[0]; // アニメーションを変数walkにセット setWalkB(mixerB.clipAction(animation)); // testB.glbを3D空間に追加; scene.add(modelB); setModelsB(modelB); }, (xhr) => { console.log("ロード中"); }, (error) => { console.error("読み込めませんでした"); } ); // 3D空間の光! const pointLight = new PointLight(0xffffff, 2, 1000, 1); //一点からあらゆる方向への光源(色, 光の強さ, 距離, 光の減衰率) pointLight.position.set(0, 200, 200); //配置される座標 (x,y,z) scene.add(pointLight); //3D空間に追加 // カメラが映し出す設定(視野角, アスペクト比, near, far) const camera = new PerspectiveCamera(45, width / height, 1, 1000); setCameras(camera); // カメラの初期座標 let cameraInitialPositionX = 0; let cameraInitialPositionY = 2; let cameraInitialPositionZ = 7; // カメラの座標 camera.position.set( cameraInitialPositionX, cameraInitialPositionY, cameraInitialPositionZ ); const render = () => { requestAnimationFrame(render); // アニメーション moveUd関数、moveLr関数でカメラ座標が移動 renderer.render(scene, camera); // レンダリング //Animation Mixerを自身と相手ともに実行 if (mixerA) { mixerA.update(clockA.getDelta()); } if (mixerB) { mixerB.update(clockB.getDelta()); } gl.endFrameEXP(); // 現在のフレームを表示する準備ができていることをコンテキストに通知するpresent (Expo公式) }; render(); }} /> </View> <View style={{ flexDirection: "row", alignSelf: "center" }}> <Positon // Position.jsからonMoveを受け取ってmove関数を実行 onMove={(data) => { move({ x: (data.x - 60) / 1000, y: (data.y - 60) / 1000, }); }} // Position.jsからonEndを受け取ってend関数を実行 onEnd={end} /> </View> </> ); } (5)終わりに 座標と向き、歩いているか否かをSocket通信で同期することで、React Nativeでのマルチプレイが可能になりました。VRoidで作成したキャラクターを使ったチャットやゲームも作れそうです。とはいえ、そのようなアプリはUnityで作成することが多いかと思います。Reactで作成するメリットがあれば良いな

Viewing all articles
Browse latest Browse all 9409

Trending Articles