Why does walk animation work well and go back to the default posture?

i’m in the process of developing a multiplayer app and studying.
I’m checking the avatar’s movements based on the WASD inputs from the keyboard and providing walk or standing animations accordingly.
walk animation works fine when in the IsMove state, but after a while, it returns to the default posture.
Why is it doing this?

below is my avatar component code

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.2.3 public/models/city city.glb -o src/components/city/index.jsx -r public
*/

import { Html, useAnimations, useGLTF } from '@react-three/drei';
import { useEffect, useMemo, useRef, useState } from 'react';
import { SkeletonUtils } from 'three-stdlib';
import { Vector3 } from '../../lib/three';
import { RapierRigidBody, RigidBody } from '@react-three/rapier';
import { socket } from '../../lib/socket';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';

interface AvatarProps {
  url: string;
  id: string;
  nickname: string;
  speed?: number;
  direction?: InstanceType<typeof Vector3>;
  frontVector?: InstanceType<typeof Vector3>;
  sideVector?: InstanceType<typeof Vector3>;
  position?: InstanceType<typeof Vector3>;
}

interface PressedType {
  back: boolean;
  forward: boolean;
  left: boolean;
  right: boolean;
  jump: boolean;
}

const PRESSED_INITIAL_STATE = {
  back: false,
  forward: false,
  left: false,
  right: false,
  jump: false,
};

type ANIMATIONS_STATE = 'M_Standing_Idle_001' | 'M_Walk_001';

const Avatar = ({
  url,
  id,
  nickname,
  speed = 3,
  direction = new Vector3(),
  frontVector = new Vector3(),
  sideVector = new Vector3(),
  ...props
}: AvatarProps) => {
  const ref = useRef<InstanceType<typeof RapierRigidBody>>(null);
  const avatar = useRef<InstanceType<typeof THREE.Group>>(null);
  const { scene } = useGLTF(url);
  const [{ forward, back, left, right }, setPressed] = useState<PressedType>(
    PRESSED_INITIAL_STATE
  );

  // animation
  const { animations: walkAnimation } = useGLTF('/animations/M_Walk_001.glb');
  const { animations: idleAnimation } = useGLTF(
    '/animations/M_Standing_Idle_001.glb'
  );
  const { actions } = useAnimations(
    [walkAnimation[0], idleAnimation[0]],
    avatar
  );
  const [animation, setAnimation] = useState<ANIMATIONS_STATE>(
    'M_Standing_Idle_001'
  );

  // memorized position
  const position = useMemo(() => props.position, []);

  // Skinned meshes cannot be re-used in threejs without cloning them
  const clone = useMemo(() => SkeletonUtils.clone(scene), [scene]);

  const onPlayerMove = (value: any) => {
    const { id: socketId, pressed: newPressed } = value;
    if (socketId === id) {
      setPressed(newPressed);
    }
  };

  useEffect(() => {
    clone.traverse((child) => {
      if (child.isObject3D) {
        child.castShadow = true;
        child.receiveShadow = true;
      }
    });
  }, [clone]);

  useEffect(() => {
    actions[animation]?.reset().fadeIn(0.25).play();

    return () => {
      actions[animation]?.fadeOut(0.25);
    };
  }, [animation, url]);

  useEffect(() => {
    socket.on('playerMove', onPlayerMove);

    return () => {
      socket.off('playerMove', onPlayerMove);
    };
  }, [id]);

  useFrame((_state) => {
    const hips = avatar.current?.getObjectByName('Hips');
    hips?.position.set(0, hips.position.y, 0);

    if (!(avatar.current && ref.current)) return;

    const velocity = ref.current.linvel();

    const isMove = back || forward || left || right;

    frontVector.set(0, 0, Number(back) - Number(forward));
    sideVector.set(Number(left) - Number(right), 0, 0);

    direction
      .subVectors(frontVector, sideVector)
      .normalize()
      .multiplyScalar(speed);

    ref.current.setLinvel(
      { x: direction.x, y: velocity.y, z: direction.z },
      true
    );

    if (direction.lengthSq() > 0) {
      const quaternion = new THREE.Quaternion();
      quaternion.setFromUnitVectors(
        new THREE.Vector3(0, 0, 1),
        direction.clone().normalize()
      );

      avatar.current.quaternion.copy(quaternion);
    }

    if (isMove) {
      setAnimation('M_Walk_001');
    } else {
      setAnimation('M_Standing_Idle_001');
    }
  });

  return (
    <RigidBody lockRotations ref={ref} position={position} type="dynamic">
      <Html
        center
        style={{
          color: '#ffffff',
        }}
        position={new Vector3(0, 2, 0)}
      >
        {nickname}
      </Html>
      <group name={`player-${id}`} dispose={null}>
        <primitive object={clone} ref={avatar} />
      </group>
    </RigidBody>
  );
};

export default Avatar;

useGLTF.preload('/animations/M_Walk_001.glb');
useGLTF.preload('/animations/M_Standing_Idle_001.glb');

It looks like you don’t have the loop mode enabled on the Action?
The default should repeat but maybe you have some code that is changing it?

https://threejs.org/docs/#api/en/animation/AnimationAction.loop

Or maybe .clampWhenFinished ?

It’s also possible that the animation has some empty space at the end of it for some reason, in which case you might have to fix the animation or adjust its timing in code…

thank you for your advice xD
i’ve solved the issue

the issue was due to re-render, which occurred while managing pressed as state.
i managed it with useRef and solved it

1 Like