会议分享:用React+lingo3d开发一款游戏
一. 基础准备
- 开发语言选择react+lingo3d
- 模型,动画,音效
- 模型:Blender软件,模型素材网站:https://sketchfab.com/feed
- 动画:https://www.mixamo.com/#/
- 音效:https://sc.chinaz.com/tag_yinxiao/cf.html
- 介绍Blender软件(创建物体,物体颜色(材质属性上),点光,摄像机)
- lingo3d
- 相机(第一人称相机,第三人称相机,轨道相机)轨道相机可以触摸屏或者鼠标点击交互
- 新建一个demo开发测试
- 使用vite创建 yarn create vite
- 第三方依赖:lingo3d-react @1.2.77
- 查找天空背景:google key words: equirectangular sky / skybox background
-
<World> <OrbitCamera active enableZoom autoRotate /> <Model src="house2.glb"></Model> </World> 、
- 引入人物模型,https://sketchfab.com/feed,优点:全球最全的3的模型网站,并且所有的模型网站都是可以直接加载到网站上面展示出来的
- demo1
-
-
//step1 <World> <Cube width={9999} depth={9999} y={-100} color={'gray'}/> <Model/> </World> //step2
function App() {const [position, setPosition] = useState({x:0,y:0,z:0})
return (<World><Cubewidth={9999}depth={9999}y={-100}color='gray'onClick={(e)=>{setPosition({x:e.point.x,y:e.point.y,z:e.point.z})}}/><Modelsrc='Idle.fbx'/><Cubecolor="orange"scale={0.2}x={position.x}y={position.y}z={position.z}/><OrbitCamera active /></World>)}
-
- demo2
-
-
//step1 添加地图 第三人称相机 人物w移动 function App() { const modelRef = useRef() return ( <World> <Model src="map/cybercity_2099_v2/scene.gltf" physics="map" scale={120} /> <ThirdPersonCamera active mouseControl> <Model src='Idle.fbx' physics="character" ref={modelRef} intersectIDs={['orangeBox']} onIntersect={()=>{ console.log(1) setMove(false) }} animations={{idle:'Idle.fbx',running:'Running.fbx'}} animation={"idle"} /> </ThirdPersonCamera> <Keyboard onKeyPress={key => { if (key === "w") { modelRef.current?.moveForward(-10) } else if (key === "Space"){ modelRef.current!.velocity.y = 5 } else if (key === "f"){ modelRef.current!.velocity.y = 5 } }} /> <Skybox texture='sky.jpg'/> </World> ) } //step2 添加状态机 改变动作 @xstate/react 和 xstate vscode插件xState 可视化,
//stateMachines/postMachineimport { createMachine } from "xstate";
export default createMachine({states: {"idle": {on: {KEY_W_DOWN: "running",KEY_SPACE_DOWN: "jumping",KEY_F_DOWN: "flying"}},"running": {on: {KEY_W_UP: "idle",KEY_SPACE_DOWN: "jumping",KEY_F_DOWN: "flying"}},"jumping": {on: {LANDED: "idle",KEY_F_DOWN: "flying"},entry: "enterJumping"},"flying": {on: {LANDED: "idle"},entry: "enterFlying"}},initial: "idle"})//app,tsx
const [pose, sendPose] = useMachine(poseMachine)<Modelphysics="character"src="Idle.fbx"animations={{idle: 'Idle.fbx',running: 'Running.fbx',jumping: "Jumping.fbx",flying: "Flying.fbx",}}animation={pose.value as any}/><KeyboardonKeyPress={key => {if (key === "w") {sendPose("KEY_W_DOWN")} else if (key === "Space"){sendPose("KEY_SPACE_DOWN")} else if (key === "f"){sendPose("KEY_F_DOWN")}}}onKeyUp={key => {if (key === "w")sendPose("KEY_W_UP")}}/>//添加修改app.tsx 使用useMef() bot.onLoop函数每秒执行60次import { useMachine } from '@xstate/react'const modelRef = useMef()const [pose, sendPose] = useMachine(poseMachine, {actions: {enterJumping: () => {const bot = modelRef.currentif (bot === null) return
bot.velocity.y = 10
bot.onLoop = () => {if (bot.velocity.y === 0) {bot.onLoop = undefinedsendPose("LANDED")}}},enterFlying: () => {const bot = modelRef.currentif (bot === null) return
bot.onLoop = () => {if (bot.velocity.y === 0) {bot.onLoop = undefinedsendPose("LANDED")}}}}})<Modelphysics="character"ref={modelRef}src="Idle.fbx"animations={{idle: 'Idle.fbx',running: 'Running.fbx',jumping: "Jumping.fbx",flying: "Flying.fbx",}}animation={pose.value as any}/><KeyboardonKeyPress={key => {if (key === "w") {sendPose("KEY_W_DOWN")modelRef.current?.moveForward(-10)} else if (key === "Space"){sendPose("KEY_SPACE_DOWN")} else if (key === "f"){sendPose("KEY_F_DOWN")modelRef.current!.velocity.y = 2}}}onKeyUp={key => {if (key === "w")sendPose("KEY_W_UP")}}/>
-
-
- demo3 添加准星+添加汽车
-
//添加汽车 <Model ref={carRef} physics="character" id="car" src="pixel_car/scene.gltf" scale={2} x={-2319.68} y={-3855.55} z={-10600.00} /> //添加准星 <Reticle color="white" variant={7} />
第三人称绑定const xSpring = useSpring({ to: camX, bounce: 0 })const ySpring = useSpring({ to: camY, bounce: 0 })const zSpring = useSpring({ to: camZ, bounce: 0 })//添加显示
<Modelref={carRef}physics="character"id="car"src="pixel_car/scene.gltf"scale={2}x={-2319.68}y={-3855.55}z={-10600.00}><Findname="GLTF_SceneRootNode"><HTML><div style={{ color: "white" }}><div style={{ fontWeight: "bold", fontSize: 20 }} duration={1000}>一款霸气的汽车</div><div>按G上下车</div></div></HTML></Find></Model>
二. 正式开发
- demo1 鼠标点击控制人物移动
-
import { useState,useRef } from 'react' import './App.css' import { World,Cube,Model, OrbitCamera, Skybox, useLoop } from 'lingo3d-react' function App() { const [position, setPosition] = useState({x:0,y:0,z:0}) const [move, setMove] = useState(false) const modelRef = useRef() useLoop(()=>{ const model = modelRef.current model.moveForward(-5) },move) return ( <World> <Cube width={9999} depth={9999} y={-200} onClick={ (e)=>{ setMove(true); setPosition({x:e.point.x, y:0, z:e.point.z}) const model = modelRef.current model.lookAt(e.point) } } /> <Model ref={modelRef} src="Idle.fbx" animations={{idle:'Idle.fbx',running:'Running.fbx'}} animation={move?"running" : "idle"} intersectIDs={['orange']} onIntersect={()=>{ setMove(false)}} /> <OrbitCamera active /> <Cube scale={0.5} id="orange" color="orange" x={position.x} y={position.y} z={position.z}/> <Skybox texture="sky.jpg"/> </World> ) } export default App
- demo2 键盘+状态管理机(@xstate/react 和 xstate),键盘控制人物移动
-
import { useMachine } from '@xstate/react' import { useRef } from 'react' import './App.css' import poseMachine from './stateMachines/postMachine' import { World, Cube, Model, OrbitCamera, Skybox, useLoop, Editor, FirstPersonCamera, ThirdPersonCamera, Keyboard } from 'lingo3d-react' function App() { const botRef = useRef() const [pose, sendPose] = useMachine(poseMachine, { actions: { enterJumping: () => { const bot = botRef.current if (bot === null) return bot.velocity.y = 10 bot.onLoop = () => { if (bot.velocity.y === 0) { bot.onLoop = undefined sendPose("LANDED") } } }, enterFlying: () => { const bot = botRef.current if (bot === null) return bot.onLoop = () => { if (bot.velocity.y === 0) { bot.onLoop = undefined sendPose("LANDED") } } } } }) return ( <World > <Model src="map/cybercity_2099_v2/scene.gltf" scale={120} physics="map" innerRotationY={180} /> <ThirdPersonCamera active mouseControl> <Model physics="character" ref={botRef} src="Idle.fbx" animations={{ idle: 'Idle.fbx', running: 'Running.fbx', jumping: "Jumping.fbx", flying: "Flying.fbx", }} animation={pose.value as any} /> </ThirdPersonCamera > <Skybox texture="sky.jpg" /> {/* <Editor/> */} <Keyboard onKeyPress={key => { if (key === "w") { sendPose("KEY_W_DOWN") botRef.current?.moveForward(-10) } else if (key === "Space"){ sendPose("KEY_SPACE_DOWN") } else if (key === "f"){ sendPose("KEY_F_DOWN") botRef.current.velocity.y = 2 } }} onKeyUp={key => { if (key === "w") sendPose("KEY_W_UP") }} /> </World> ) } export default App
- 状态机:
-
import { createMachine } from "xstate"; export default createMachine({ states: { "idle": { on: { KEY_W_DOWN: "running", KEY_SPACE_DOWN: "jumping", KEY_F_DOWN: "flying" } }, "running": { on: { KEY_W_UP: "idle", KEY_SPACE_DOWN: "jumping", KEY_F_DOWN: "flying" } }, "jumping": { on: { LANDED: "idle", KEY_F_DOWN: "flying" }, entry: "enterJumping" }, "flying": { on: { LANDED: "idle" }, entry: "enterFlying" } }, initial: "idle" })
- demo3 瞄准器+制作汽车彩蛋+场景优化(@lincode/react-anim-text) useSpring(弹簧--)
import { useMachine } from '@xstate/react' import { useRef, useState } from 'react' import './App.css' import poseMachine from './stateMachines/postMachine' import { World, Cube, HTML, Model, OrbitCamera, Skybox, useLoop, Editor, FirstPersonCamera, ThirdPersonCamera, Keyboard, Reticle, useSpring, Find } from 'lingo3d-react' import AnimText from "@lincode/react-anim-text" function App() { const botRef = useRef() const carRef = useRef() const [mouseOver, setMouseOver] = useState(false) const [driveCar, setDriveCar] = useState(false) const [intersectCar, setIntersectCar] = useState(false) const [pose, sendPose] = useMachine(poseMachine, { actions: { enterJumping: () => { const bot = botRef.current if (bot === null) return bot.velocity.y = 10 bot.onLoop = () => { if (bot.velocity.y === 0) { bot.onLoop = undefined sendPose("LANDED") } } }, enterFlying: () => { const bot = botRef.current if (bot === null) return bot.onLoop = () => { if (bot.velocity.y === 0) { bot.onLoop = undefined sendPose("LANDED") } } } } }) const camX = mouseOver ? 25 : 0 const camY = mouseOver ? 50 : 50 const camZ = mouseOver ? 50 : 200 const xSpring = useSpring({ to: camX, bounce: 0 }) const ySpring = useSpring({ to: camY, bounce: 0 }) const zSpring = useSpring({ to: camZ, bounce: 0 }) return ( <> <World > <Model src="map/cybercity_2099_v2/scene.gltf" scale={120} physics="map" innerRotationY={180} > {/* <Find name="Hovercars_Holograms_0" outline={mouseOver} onMouseOver={() => setMouseOver(true)} onMouseOut={() => setMouseOver(false)} > {mouseOver && ( <HTML> <div style={{ color: "white" }}> <AnimText style={{ fontWeight: "bold", fontSize: 20 }} duration={1000}> 可乐汉堡店铺 </AnimText> <AnimText duration={1000}> hamburger coca-cola </AnimText> </div> </HTML> )} </Find> */} </Model> <ThirdPersonCamera active={!driveCar} mouseControl innerY={ySpring} innerZ={zSpring} innerX={xSpring} > <Model physics="character" ref={botRef} src="Idle.fbx" animations={{ idle: 'Idle.fbx', running: 'Running.fbx', jumping: "Jumping.fbx", flying: "Flying.fbx", }} animation={pose.value as any} intersectIDs={["car"]} onIntersect={() => setIntersectCar(true)} onIntersectOut={() => setIntersectCar(false)} /> </ThirdPersonCamera > <ThirdPersonCamera active={driveCar} mouseControl innerY={ySpring} innerZ={zSpring} innerX={xSpring} > <Model ref={carRef} physics="character" id="car" src="pixel_car/scene.gltf" scale={2} x={-2319.68} y={-3855.55} z={-10600.00} > <Find name="GLTF_SceneRootNode" outline={!driveCar && mouseOver} onMouseOver={() => setMouseOver(true)} onMouseOut={() => setMouseOver(false)} > {!driveCar && mouseOver && ( <HTML> <div style={{ color: "white" }}> <AnimText style={{ fontWeight: "bold", fontSize: 20 }} duration={1000}> 一款霸气的汽车 </AnimText> <AnimText duration={1000}> 按G上下车 </AnimText> </div> </HTML> )} </Find> </Model> </ThirdPersonCamera > <Skybox texture="sky.jpg" /> {/* <Editor/> */} <Keyboard onKeyPress={key => { console.log(key) if (driveCar) { if (key === "w") { carRef.current?.moveForward(-20) } } else { if (key === "w") { sendPose("KEY_W_DOWN") botRef.current?.moveForward(-10) } else if (key === "Space") { sendPose("KEY_SPACE_DOWN") } else if (key === "f") { sendPose("KEY_F_DOWN") botRef.current.velocity.y = 2 } } }} onKeyUp={key => { if (key === "w") { sendPose("KEY_W_UP") } else if (key === "g") { if (driveCar) { botRef.current!.x = carRef.current?.x botRef.current!.y = carRef.current?.y botRef.current!.z = carRef.current?.z setDriveCar(false) } else if (intersectCar) { setDriveCar(true) } } }} /> {/* <Editor/> */} </World> {!driveCar && <Reticle color="white" variant={7} />} </> ) } export default App
🔸3D模型下载网站:https://sketchfab.com/feed
🔸3D人物动作绑定:www.mixamo.com
🔸3D角色生产工具:https://readyplayer.me/
🔸模型压缩网站:gltf.report
🔸查找天空背景:google key words: equirectangular sky / skybox background
🔸材质贴图素材:https://www.textures.com
🔸hdr素材库(环境贴图): https://polyhaven.com
🔸二次元风3D角色生产软件VRoid Studio: https://vroid.com/en/studio