bberak / react-game-engine

A lightweight Game Engine for the web (React) 🕹⚡🎮
MIT License
413 stars 24 forks source link

Button states from systems. #5

Closed michaelcuneo closed 4 years ago

michaelcuneo commented 4 years ago

I've set up a HUD on my game engine implementation, and the HUD contains three interactive areas, a slider switch for RESIZE/MOVE, and two buttons, RESET and SUBMIT.

Trying to get feedback from the HUD System, so that whatever variables are set in the HUD, can be used within another System. Cannot seem to send/receive props from that side of things... should I implement that area of my HUD into a redux saga to set/get the state?

main.js

    <GameEngine
      style={style}
      key="GameEngine"
      systems={Systems}
      entities={Entities()}
    />,

entities.js

export default async () => {
  const redSphere = Sphere();
  const blueSphere = Sphere();
  const greenSphere = Sphere();
  const feedbackHud = FeedbackHUD();
  const switchHud = SwitchHUD();

  const entities = {
    redSphere,
    blueSphere,
    greenSphere,
    feedbackHud,
    switchHud,
  };

  return entities;
};

switchhud.js

const SwitchHUD = (entities, args) => {
  const { switchHud } = entities;

  if (switchHud) {
    switchHud.gamepadController = args.gamepadController;
    switchHud.keyController = args.keyController;
    switchHud.mouseController = args.mouseController;
  }

  return entities;
};

export default SwitchHUD;

switchHud.js

const Desktop = ({ children }) => {
  const isDesktop = useMediaQuery({ minWidth: 992 });
  return isDesktop ? children : null;
};
const Mobile = ({ children }) => {
  const isMobile = useMediaQuery({ minWidth: 0, maxWidth: 991 });
  return isMobile ? children : null;
};

class SwitchHUDRenderer extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      value: false,
      reset: false,
      submit: false,
    };
  }

  render() {
    return [
      <Desktop key="Desktop">
        <div style={css.desktopHud}>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                RESIZE &nbsp;&nbsp;
                <InputSwitch
                  checked={this.state.value}
                  onChange={e => this.setState({ value: e.value })}
                />
                &nbsp;&nbsp; MOVE
              </span>
            </Text>
          </h1>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                <Button
                  label="RESET"
                  className="p-button-danger"
                  onChange={e => this.setState({ reset: e.value })}
                />
              </span>
            </Text>
          </h1>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                <Button
                  label="SUBMIT"
                  className="p-button-standard"
                  onChange={e => this.setState({ submit: e.value })}
                />
              </span>
            </Text>
          </h1>
        </div>
      </Desktop>,
      <Mobile key="Mobile">
        <div style={css.mobileHud}>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                RESIZE &nbsp;&nbsp;
                <InputSwitch
                  checked={this.state.value}
                  onChange={e => this.setState({ value: e.value })}
                />
                &nbsp;&nbsp; MOVE
              </span>
            </Text>
          </h1>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                <Button
                  label="RESET"
                  className="p-button-danger"
                  onChange={e => this.setState({ reset: e.value })}
                />
              </span>
            </Text>
          </h1>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                <Button
                  label="SUBMIT"
                  className="p-button-standard"
                  onChange={e => this.setState({ submit: e.value })}
                />
              </span>
            </Text>
          </h1>
        </div>
      </Mobile>,
    ];
  }
}

const css = {
  desktopHud: {
    display: 'flex',
    flexDirection: 'row',
    position: 'fixed',
    userDrag: 'none',
    userSelect: 'none',
    alignContent: 'center',
    alignItems: 'center',
    width: '100vw',
    bottom: '100px',
    justifyContent: 'space-around',
    padding: '1em',
    fontSize: '9pt',
    backgroundColor: '#2d2e2f',
    color: 'white',
  },
  mobileHud: {
    display: 'flex',
    flexDirection: 'row',
    position: 'fixed',
    userDrag: 'none',
    userSelect: 'none',
    alignContent: 'center',
    alignItems: 'center',
    width: '100vw',
    bottom: '0px',
    justifyContent: 'space-around',
    padding: '1em',
    fontSize: '9pt',
    backgroundColor: '#2d2e2f',
    color: 'white',
  },
};

export default (reset, value, submit) => ({
  renderer: <SwitchHUDRenderer reset={reset} value={value} submit={submit} />,
});

Screenshot of current implementation...

https://haldor.michaelcuneo.com.au/ThreeCircles2.PNG

Is there a way to pull the data in and out of the engine in this case, or should I add saga's, actions, selectors, etc, then just pull the state into the Resize / Move systems, as selected data?

i.e. Resize, runs action to set State of movementMode, movement mode becomes Resize, ResizeSystem sets state to the selected prop movementMode, which is a string 'resize' if(resize) perform resize movements... else return...

Just thought I'd ask if there was something in the system to pull the state in and out before I write tonnes of sagas and such.

bberak commented 4 years ago

Hi @michaelcuneo,

Okay - this is an interesting scenario..

The engine comes with a simple messaging channel, which at a high level looks like:

const System1 = (entities, { dispatch, events }) => {

  if (events.length)
    console.log(events) //-- MessageFromHUD

  dispatch({ type: "MessagFromSystem1"})
};

class MyGame extends React.Componet {

  onMessageFromGameEngine = e => {
    console.log(e); //-- MessagFromSystem1
  };

  onMessageFromHUD = e => {
    this.refs.gameEngine.dispatch({ type: "MessageFromHUD" });
  };

  render() {
    return (
      <View>
        <GameEngine ref={"gameEngine"} onEvent={onMessageFromGameEngine} systems={[System1]} />
        <MyHud onChange={onMessageFromHUD} />
      </View>
    );
  }
}

NOTE: The messages are schema less, you can dispatch whatever data you want.

Perhaps that might simplify things a bit for you? Ideally, I'd like people to be able to focus on their game or interactive scene without writing tonnes of boilerplate redux/saga/flux code..

Just thinking out loud.. Perhaps I can update the engine to automatically make the dispatch function available to the renderers? That way you can use the dispatch function in your event handler callbacks? Actually, we can probably achieve this now without an engine update:

const SwitchHUD = (entities, args) => {
  const { switchHud } = entities;

  if (switchHud) {
    switchHud.gamepadController = args.gamepadController;
    switchHud.keyController = args.keyController;
    switchHud.mouseController = args.mouseController;
    switchHud.dispatch = args.dispatch;
  }

  return entities;
};

export default SwitchHUD;

class SwitchHUDRenderer extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return [
      <Desktop key="Desktop">
        <div style={css.desktopHud}>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                RESIZE &nbsp;&nbsp;
                <InputSwitch
                  checked={props.value}
                  onChange={e => props.dispatch({ type: "resize", value: !props.value })}
                />
                &nbsp;&nbsp; MOVE
              </span>
            </Text>
          </h1>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                <Button
                  label="RESET"
                  className="p-button-danger"
                  onChange={e => props.dispatch({ type: "reset" })}
                />
              </span>
            </Text>
          </h1>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                <Button
                  label="SUBMIT"
                  className="p-button-standard"
                  onChange={e => props.dispatch({ type: "submit" })}
                />
              </span>
            </Text>
          </h1>
        </div>
      </Desktop>
    ];
  }
}

Let me know what you think.. I realise I was jumping around all over the place - so apologies if I've misunderstood what you were trying to achieve..

bberak commented 4 years ago

The other thing you can do is mutate the HUD state directly... Eg:

class SwitchHUDRenderer extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return [
      <Desktop key="Desktop">
        <div style={css.desktopHud}>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                RESIZE &nbsp;&nbsp;
                <InputSwitch
                  checked={props.data.value}
                  onChange={props.toggle}
                />
                &nbsp;&nbsp; MOVE
              </span>
            </Text>
          </h1>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                <Button
                  label="RESET"
                  className="p-button-danger"
                  onChange={props.reset}
                />
              </span>
            </Text>
          </h1>
          <h1>
            <Text fontSize={['10px', '14px', '20px']}>
              <span>
                <Button
                  label="SUBMIT"
                  className="p-button-standard"
                  onChange={props.submit}
                />
              </span>
            </Text>
          </h1>
        </div>
      </Desktop>
    ];
  }
}

export default () => {

const data = { value: false }
const reset = () => data.value = false;
const toggle = () => data.value = !data.value;
const submit = () => ...

return { data, reset, toggle, submit, renderer: SwitchHUDRenderer };
}

Other systems will be able to read whether the mode is move or resize by inspecting the data.value property of the switchHud entity (or whatever you've called your HUD entity).

michaelcuneo commented 4 years ago

Ahh yeah that looks like it'll be way more successful... I was trying everything I could to get the state into the right area... and this one was the closest that I got...

const SwitchHUD = (entities) => {
  const { switchHud } = entities;

  if (switchHud) {
    switchHud.mode = switchHud.mode || false;
  }

  return entities;
};

export default SwitchHUD;

I had a mode in the SwitchHud entity but it was always showing undefined... because args was never anything, even if I set it to something initially and pushed the switchHud mode out by

return { mode => renderer: SwitchHUDRenderer };

Excellent.

I'm used to using a messenger channel Hub for my auth, as a provider, I just wrap it around the and listen for messages and change the state accordingly to push it through the entire app, so this looks like it'll be perfect. I'll make up an implementation and see how it goes. Thanks for all your help on this.

bberak commented 4 years ago

Don't mention it @michaelcuneo - let me know how it goes.

michaelcuneo commented 4 years ago

Worked perfectly... but I'm worried a bit about the setting state on the return. I tried solving that by wrapping the function with some state hooks, but realised that the state hooks would still need to reassign the props on return. So I guess there are some scenarios when assigning state on return are acceptable... I just threw in an es-lint ignore...

My switchHud now has this as the switch value and return value...

<InputSwitch checked={props.data.value} onChange={props.toggle} />

export default () => {
  const data = { value: false };
  // eslint-disable-next-line no-return-assign
  const toggle = () => (data.value = !data.value);
  return { data, toggle, renderer: SwitchHUDRenderer };
};

I can then use it in the Resize and Drag systems with a simple

const { switchHud } = entities;
if (!switchHud.data.value) drag;
if (switchHud.data.value) resize;

Data goes through well... quite fast and responsive. I can see now how I can add another two values there now for the reset and submit then just perform some tasks accordingly.