pmndrs / use-cannon

👋💣 physics based hooks for @react-three/fiber
https://cannon.pmnd.rs
2.78k stars 156 forks source link

improve usage of collide event #133

Closed a-type closed 3 years ago

a-type commented 3 years ago

Hello there! I've been looking into building games with React, and I love the fact that this library runs the physics engine in a worker. However, I found that even some basic game ideas require a more fleshed-out understanding of contact events than is currently provided, both in documentation and provided values.

This is an initial attempt at improving the data provided from the collide event so it can be used more easily. The main questions I wanted to answer:

  1. What was the point of contact?
  2. What was the normal of contact on the surface of the object this body collided with?
  3. When did a contact start? When did it end?

To wit I added two new callbacks to body hooks:

This is pretty much all the info Cannon provides with its corresponding start/end events, but it's enough to work with. At least with the identity of the other body, you can look up the collision and flag it as "beginning" or "ended."

Here's how I'm currently using my fork with local testing (unoptimized, rough sketch):


type ContactEvent = CollideEvent & { beginning: boolean; ended: boolean };

export type ContactsState = {
  active: ContactEvent[];
};

export const useContacts = () => {
  // have to collect these separately since they come from events
  // which might fire at any time during the frame - so I collect them
  // here and then copy them to the main collection at the start of each frame.
  const [collectedContacts] = React.useState<{
    active: ContactEvent[];
    beginIds: Set<string>;
    endedIds: Set<string>;
  }>(() => ({
    active: [],
    beginIds: new Set<string>(),
    endedIds: new Set<string>(),
  }));
  const [contacts] = React.useState<ContactsState>(() => ({
    active: [],
  }));

  const onCollide = React.useCallback(
    (e: CollideEvent) => {
      collectedContacts.active.push({
        ...e,
        beginning: false,
        ended: false,
      });
    },
    [collectedContacts],
  );
  const onCollideBegin = React.useCallback(
    (e: CollideBeginEvent) => {
      collectedContacts.beginIds.add(e.body.uuid);
    },
    [collectedContacts],
  );
  const onCollideEnd = React.useCallback(
    (e: CollideEndEvent) => {
      collectedContacts.endedIds.add(e.body.uuid);
    },
    [collectedContacts],
  );

  useFrame(() => {
    contacts.active = collectedContacts.active.reduce((list, contact) => {
      // drop ended contacts
      if (contact.ended) {
        return list;
      }
      if (collectedContacts.endedIds.has(contact.body.uuid)) {
        contact.ended = true;
      }
      if (collectedContacts.beginIds.has(contact.body.uuid)) {
        contact.beginning = true;
      } else {
        contact.beginning = false;
      }
      list.push(contact);
      return list;
    }, new Array<ContactEvent>());
    // copying back to collected - this keeps active collisions
    // "alive" since onCollide is really only called once per collision.
    // The idea is to continually provide the collision during its lifecycle
    // to code that cares about it.
    collectedContacts.active = contacts.active;
    collectedContacts.beginIds = new Set<string>();
    collectedContacts.endedIds = new Set<string>();
  // frame priority is set very low, so this happens before other frame handlers
  }, -100);

  return [
    contacts,
    {
      onCollide,
      onCollideBegin,
      onCollideEnd,
    },
  ] as const;
};

// In a component
const [contacts, handlers] = useContacts();
const [ref] = useSphere({ mass: 1, ...handlers });

useFrame(() => {
  // now I can use contacts.active here to check active collisions,
  // new collisions vs. ended ones, etc.
});

This is still quite a bit of code to add on top of the library, though, so maybe there would be room to expand this library's usage to manage these flags for you? That is, if you find this addition worthwhile at all. I know this library seems a bit more casual in terms of usage and maybe this level of complexity isn't something you want to introduce.

stockhuman commented 3 years ago

Hey, thanks for this, and pardon the very late reply. What you have looks useful to the use-cases this library seems to explore most often, so I'm for it. If it's ready, perhaps Paul should take a look at it given that he may feel that, as you say - it may be out of scope.

drcmda commented 3 years ago

no, please, go ahead. if the conflicts are solved we should merge

a-type commented 3 years ago

Alright, I'll see if I can get back in context to do that :)

drcmda commented 3 years ago

there are no breaking changes in this, is this correct? just the extended cannon functionality?

a-type commented 3 years ago

In theory! Are there tests I can run against it to be sure? It's been so long since I opened this PR, but I did my best to merge with recent changes.

The goal was to add some additional contact info to the collide event, and add two new events (collideStart and collideEnd) which can help track lifecycles of collisions.

drcmda commented 3 years ago

only the examples,

yarn build cd examples yarn start

if everything works lets merge, and if something comes up we can figure afterwards. :-)

a-type commented 3 years ago

Yup, they all seem to work as intended!

drcmda commented 3 years ago

awesome, thanks a lot! :-)

ps. you dont happen to have some kind of demo that we could use to help people understand the functionality they can now use?

a-type commented 3 years ago

Afraid not... to be honest I built this anticipating using r3f for more advanced game dev, but then I found https://rapier.rs and it's become my go-to JS physics engine instead.