threejs 实现3D游戏(6)——第一视角&场景交互
概述
游戏中出现第一视角切换的功能和与场景中物品的交互等是常见的功能,这次我们来实现这2个功能。 为了方便,我暂时去掉了多人同屏相关的代码,这个代码是从联机前的版本基础迭代而来。
第一视角
方案一
如果不使用ecctrl,可以直接使用three中自带的个人视角相机来实现。关于这种实现官网有相关的案例参考无需赘述:
这种实现不需要借助其他依赖库,可进行很强的自定义,性能也相对较优。
方案二
而我们已经习惯使用ecctrl了,而在ecctrl中也很简单只需要配置相关的参数即可。关键的是它自带pc手机端适配,你不需要对控制做太多处理工作,不需要自己实现一个虚拟摇杆来适配手机(实际上自己实现也不复杂)。
综上我们这次使用方案二。
我们直从github上 ecctrl 库的说明上找到相关配置参数
同时去除掉玩家关于动画部分的设置,重新添加音频的lisenter到音频自身的模型上(因为ecctrl下没有其他实际模型了)你也可以添加一个透明模型放在Ecctrl组件中,将lisenter绑定到它身上。
...
export default function Player() {
const bgmRef = useRef<THREE.PositionalAudio>(null); // bgm音频
const stepsRef = useRef<THREE.PositionalAudio>(null); // 脚步音频
const { update } = useModels();
useKeyboardControls((state) => move(state));
// 存储玩家的位置
const player = useThree((state) => state.scene).getObjectByName("player");
useEffect(() => {
if (!player) return;
update({ playerPos: player.position });
if (!bgmRef?.current) return;
const bgmObj = bgmRef.current;
bgmObj.add(bgmObj.listener);
return () => {
bgmObj.remove(bgmObj.listener);
};
}, [player]);
// 玩家移动脚本
function move(state: { [key: string]: boolean }): boolean {
if (!stepsRef?.current) return false;
const { forward, backward, leftward, rightward } = state;
if (forward || backward || leftward || rightward) {
!stepsRef.current.isPlaying && stepsRef.current?.play();
} else {
stepsRef.current?.stop();
}
return false;
}
return (
<Ecctrl
autoBalance={false}
position={[0, 7, 0]}
floatHeight={0.01}
camInitDis={-0.01}
camMinDis={-0.01}
camMaxDis={-0.01}
camFollowMult={100}
turnVelMultiplier={1}
turnSpeed={100}
mode="CameraBasedMovement"
name="player"
>
<PositionalAudio
ref={bgmRef}
url={AUDIOS.song}
distance={1}
autoplay
loop
/>
<PositionalAudio ref={stepsRef} url={AUDIOS.steps} distance={1} loop />
</Ecctrl>
);
}
场景交互
让用户与场景中的模型进行交互。我们这里实现简单的2种。 第一种主动用鼠标或手指点击了某一个模型触发的事件; 第二种用户到达某一个区域时自动触发的事件。我们分别称为模型事件和区域事件。
模型事件
当用户点击模型时触发的事件,这个相对比较简单我们可以直接在需要互动的模型上添加事件监听即可。 假如我们为场景添加一个点击后会弹窗的宝箱。 我们在models文件夹下创建一个box模型,再加载到models下的index.tsx中
...
export default function Box(props: MeshProps) {
const meshRef = useRef<THREE.Mesh>(null!);
const { update } = usePages();
const [hovered, setHover] = useState(false);
useFrame((_state, delta) => (meshRef.current.rotation.x += delta));
useCursor(hovered);
return (
<mesh
{...props}
ref={meshRef}
scale={hovered ? 1.5 : 1}
onPointerDown={() =>
update({ showModal: true, content: "你点击了宝箱!" })
}
onPointerOver={() => setHover(true)}
onPointerOut={() => setHover(false)}
>
<boxGeometry args={[0.6, 0.6, 0.6]} />
<meshStandardMaterial color={hovered ? "hotpink" : "orange"} />
</mesh>
);
}
我们在pages文件夹下添加一个弹窗组件,把它加载到pages下的index.tsx中
export default function Modal() {
const { showModal, update, content } = usePages();
function close() {
update({ showModal: false });
}
function confirm() {
console.log("confirmClick");
update({ showModal: false });
}
return (
showModal && (
<div className="modal shake">
<div className="content">{content}</div>
<div className="btns">
<div className="cancel" onClick={close}>
No
</div>
<div className="confirm" onClick={confirm}>
Yes
</div>
</div>
</div>
)
);
}
区域事件
在我们之前实现的小地图中,会定时渲染玩家的位置。我们直接在这里判断玩家当前处于什么区域,再触发对应的事件。
进入 pages中hub下的miniMap中,在定时更新位置的地方添加一个监听函数。这里note是一个标志,当提醒过一次后,不再提醒。
...
// 定时更新位置
function updatePosition() {
if (!playerPos) return;
const { x, z } = playerPos;
isInArea(x,z)
...
}
function isInArea(x: number, z: number) {
if ((x > 10 || x < -10 || z > 10 || z < -10) && !note) {
// console.log(note);
note = true;
update({ showModal: true, content: "请注意,即将离开安全区域!" });
}
}
...
模型拖拽
当我们往场景中添加一些模型时,它们的位置总是不好把握的。我们如果一次次的修改位置的话实在过于浪费时间。
好在react-three/drei中为我们提供了TransformControls控件它可以让我们直接拖拽或旋转模型。
这里我们使用第二种引用对象赋值,主要是为了获取目标模型改变后的位置和角度。
export default function VideoMesh(props: Props) {
const meshRef = useRef<Object3D>(null!);
const handleChange = () => {
console.log(meshRef.current?.position);
console.log(meshRef.current?.rotation);
};
const VideoMaterial = ({ url }: { url: string }) => {
const texture = useVideoTexture(url);
return <meshBasicMaterial map={texture} toneMapped={false} />;
};
return (
<group>
<TransformControls
mode="translate"
object={meshRef.current}
onChange={handleChange}
makeDefault
/>
<mesh
scale={[1.8, 1, 1]}
position={[-2.514, 5.14, 3.56]}
rotation={[-Math.PI, 1.27, -Math.PI]}
{...props}
ref={meshRef}
>
<planeGeometry />
<VideoMaterial url="./videos/10.mp4" />
</mesh>
</group>
);
}
结语
这次我们实现了如下功能
- 第一视角游览;
- 添加场景中的事件交互;
- 为场景模型添加拖拽和旋转;
下次我们来实现场景的切换。
碎语
当日轻别意中人,山长水远知何处?
稚儿擎瓜柳棚下,细犬逐蝶窄巷中。 人间繁华多笑语,唯我空余两鬓风。
转载自:https://juejin.cn/post/7381372851121602598