React three fiberコンポーネントのonClickイベントでハマった話

React

ご無沙汰しております!

師走という事もあり諸々忙しかった為、更新が停滞しておりました・・・っ

さて今回の件ですが、

React three fiberの図形描画オブジェクトのonClickイベントを仕掛けた所、思いがけない所でハマったので開発ネタ兼備忘録として記載しておこうと思います。

作っていたもの

現在メジェワークスのサイトコンテンツを増やす為、

React three fiberの練習も兼ねて3Dキャンパスで遊べるナンバープレイスを作っておりました。

作成中ナンプレ画面(β版)

この画面では、「緑の枠線・数字が入ったブロック・ブロック上の数字」をReact three fiberのコンポーネントで描画し、並べています。

※上記のスクリーンショットでは映っていませんが、数字ブロックにマウスカーソルを合わせた時のアウトラインも描画するようになっています

各1個のブロックを描画するコードは以下のようにしていました。

import React from 'react'
import * as THREE from 'three'
import { Text, RoundedBox, Outlines, useCursor } from "@react-three/drei";
import { BlockSelecter } from '../BlocksControl';
import { useRecoilState } from 'recoil';

// ブロック1つを描画するコンポーネント 
export const DrawNumberBlock:React.FC<Props> = (props) => {
//~
//中略
//~
  const [ blockSelecter, setBlockSelecter] = useRecoilState(BlockSelecter);
  const onBlockSelect = () => setBlockSelecter({selected:!blockSelecter.selected, id:props.blockId});
  const tileColor = (blockSelecter.selected && blockSelecter.id==props.blockId)? props.selectedColor : props.color;

  return(
    <mesh position={props.position}
      onPointerOver={() => setHovered(true)}
      onPointerOut={() => setHovered(false)}
      onClick={
        onBlockSelect
      }
    >
      <RoundedBox
        ref={boxRef}
        args={[props.width, blockHeight, blockVolume]}
        radius={0.025}
        smoothness={10}
        bevelSegments={4}
        creaseAngle={0.4}
      >
        {NumText}
        <meshBasicMaterial color={tileColor}/>
        <Outlines
          color={"#550000"}
          screenspace={false}
          opacity={Number(hovered)}
          toneMapped={false}
          polygonOffset
          polygonOffsetFactor={10}
          transparent
          thickness={props.width*0.05}
          angle={Math.PI}
        />
      </RoundedBox>
    </mesh>
  );

今回の件に関わって来るポイントとしては、

数字ブロックの3Dメッシュを構成する<mesh>と、その中で設定している、<RoundedBox>・<Text>・<OutLines>が深く関係してきます。

※<Text>は前処理をしている関係で{NumText}に入れています

それぞれ以下の様なコンポーネントです。

  • <mesh>:内部の3Dオブジェクトを1つのメッシュとして定義する
  • <RoundedBox>:角丸のキューブを描画する
  • <Text>:3D空間上に平面的なフォント指定テキストを描画する
  • <Outlines>:Outlinesを設定したオブジェクトに外枠を描画する ※ブロック上にマウスホバーした時に外枠を描画するように設定

つまる所、<mesh>を土台にして、<RoundedBox>・<Text>・<Outlines>を重ねて1つの数字ブロックを構成しています。

そして、数字ブロックがクリックされた時のonClickイベントに、選択されたブロックの情報を判定するSelecter(状態管理にReact Recoilを使用)を仕込んで状態管理するコードになっています。

※クリックしたブロックは選択状態の緑に、もう一度選択すると解除

図形オブジェクト重ねた時のonClickイベントにご注意

数字ブロックは複数のコンポーネントを重ねて1つの3Dオブジェクトとして振舞っていたわけですが、

当初の実装(上記の内容)では、土台になっていた<mesh>にonClickイベントを設定していました。

今回の動作仕様として、下記を満たすべくこの様にしていました。

  • 数字ブロックを1回クリックすると選択状態にする
  • 選択状態の数字ブロックを再度クリックすると選択解除する
  • 選択状態のブロック以外が選択された時は、選択状態のブロックを解除して、新たにクリックされたブロックのみ選択状態にする

しかし、実装した数字ブロックをクリックしてみると、、、

  • 数字の無いブロックではonClickイベントが2回発生してしまい、「選択解除→選択状態→選択解除」となって選択状態にできない
  • 数字の有るブロックではonClickイベントが3回発生してしまい、「選択解除→選択状態→選択解除→選択状態」となり選択状態になる
  • 数字の有るブロックでも、ブロック内の数字が無い部分をクリックすると数字が無いブロックと同様に「選択解除→選択状態→選択解除」と2回イベントが発生する

という状況になっていました。

これは、前章で記載していた通り「<mesh>を土台にして、<RoundedBox>・<Text>・<Outlines>を重ねて1つの数字ブロックを構成」という形で作成していたので、

<mesh>のonClickイベントに反映したSelecterの処理が、マウスクリック時に重なっていた<RoundedBox>・<Text>・<Outlines>に伝播して、イベントが重複していた所為でした。。。

onClickイベントを持つオブジェクトを<mesh>でひとまとまりにした際は、トップレベルの<mesh>ではなく一番外回りを構成するジオメトリにonClickを設定する事で、onClickイベントが重複する事無く再レンダリング回数も抑える事ができます。

//~
// 前略
//~
export const BlocksControl: React.FC<Props>  = (props) =>{
  //~
  // 中略
  //~

  return(
    <mesh position={props.position}
      // <mesh>から各種イベント設定を除外して
      - onPointerOver={() => setHovered(true)}
      - onPointerOut={() => setHovered(false)}
      - onClick={
      -  onBlockSelect
      - }
    >
      <RoundedBox
        ref={boxRef}
        args={[props.width, blockHeight, blockVolume]}
        radius={0.025}
        smoothness={10}
        bevelSegments={4}
        creaseAngle={0.4}
      >
        {NumText}
        <meshBasicMaterial color={tileColor}/>
        <Outlines
          color={"#550000"}
          screenspace={false}
          opacity={Number(hovered)}
          toneMapped={false}
          polygonOffset
          polygonOffsetFactor={10}
          transparent
          thickness={props.width*0.05}
          angle={Math.PI}

      // 一番外回りのジオメトリになっているOutlinesに各種イベントを設定
          + onPointerOver={() => setHovered(true)}
          + onPointerOut={() => setHovered(false)}
          + onClick={onBlockSelect}
        />
      </RoundedBox>
    </mesh>
  );

この実装に変更する事で、イベントの重複発生が無事解消!


Reactの状態管理は気を付けるポイントも多いので、当初はSelecterの参照方法が悪くて再レンダリングが行われる等でイベントが重複しているのか、と考えたりもしましたが、

結論としてReact Three Fiberのコンポーネントを組み合わせた際、ちょっとした落とし穴があるよ。というお話でした。

onClick以外のイベントや、Three Fiberでのコンポーネントの組み合わせの方によって思いがけず躓く場面があるかもしれませんのでご注意を。。。


年内にナンプレ公開出来るかなー

コメント

タイトルとURLをコピーしました