react-native-game-engine icon indicating copy to clipboard operation
react-native-game-engine copied to clipboard

Android performance

Open lukechatton opened this issue 6 years ago • 34 comments

I was very satisfied with my prototype's animation performance on iOS. I used the rigid bodies scene from react-native-game-engine-handbook as my starting point (matter.js physics). I was rendering entities using View and Image components from react-native.

However, after running the app inside an android emulator I noticed pretty terrible performance. Animating a simple cloud image across the screen was not smooth at all. It was also apparent that matter.js was "slowing down" at some points.

In the rigid bodies scene, you update the matter.js engine inside a system. After some messing around, I noticed that updating matter.js outside of a system removed the matter.js "slowing down" issue.

Old:

const Physics = (state, { touches, time }) => {
    let engine = state["physics"].engine;

    Matter.Engine.update(engine, time.delta);

    return state;
};

New:

let now = Date.now();
setInterval(() => {
    Matter.Engine.update(engine, Date.now() - now);
    now = Date.now();
}, 0);

I know close to nothing about game dev. Does moving the physics updater outside of a system seem like an okay approach? I'm not sure how else to provide a playable experience on Android.

lukechatton avatar Mar 11 '19 04:03 lukechatton

Hmm @lukechatton, interesting find.

setInterval with 0 should place the function on the event loop, to be executed when the call stack is free. Perhaps doing this prevents MatterJS from interfering with the current frame - resulting in better perf - but I'm really just brainstorming here..

I'd also recommend trying to run your project on a physical Android device too (if at all possible) - I've found the performance on Android simulators to be poorer than on a physical device (as a rule of thumb). I just borrowed an Android device from a friend.

If you replace your cloud image with a simple coloured View - does the performance improve? I'm curious to see if the animation of the image is the cause of the performance issues (although that would surprise me).

bberak avatar Mar 11 '19 04:03 bberak

Yes I tried replacing the image with a colored View. No luck. I also tried disabling my systems to make sure it wasn't any of my code holding up the event loop.

I haven't tried running in on a physical Android device yet. I'l have to get a friend to try it out for me.

lukechatton avatar Mar 11 '19 05:03 lukechatton

When I moved matter.js updating out of a system it only prevented the physics time from slowing down. I still wasn't getting buttery smooth animations like I was getting on iOS.

lukechatton avatar Mar 11 '19 05:03 lukechatton

What happens if you remove the systems and the physics update altogether? Are you still getting janky animations? If so, I would definitely give a physical device a shot.

Otherwise, if you can share a very basic repo of your project with me, I'd be happy to run it on my simulator and see what results I get..

bberak avatar Mar 11 '19 05:03 bberak

Hi @lukechatton

Have you made any progress with this or hit any roadblocks?

bberak avatar Mar 21 '19 05:03 bberak

I haven’t been able to do much more testing since my last post. I think this can be closed though, I appreciate your help in understanding what’s going on. 👍

lukechatton avatar Mar 22 '19 14:03 lukechatton

No problems @lukechatton - feel free to re-open the issue later if need be.

Cheers!

bberak avatar Mar 24 '19 00:03 bberak

@bberak I'm having this issue on Android but in my case it is not related to matterjs at all and it happens on a real device. On iOS everything runs smooth but on Android it does not.

I made a test in debug mode and noticed that the FPS dropped from 60 to around 30 only with this code:

<View style={{ flex: 1 }}>
	<GameEngine
	style={styles.engine}
	systems={[]}
	entties={{}} />
</View>

I also checked if my render method was being called more than once and it's not. Is there a reason for this FPS drop? How can I get better results with this?

reyalpsirc avatar Apr 15 '20 08:04 reyalpsirc

@bberak It seems that if I use the GameLoop instead, I get better performance. I stored the entities data on variables of my class and then used this to test and got much more performance:

private renderEntity = (entity) => {
  if (!entity) return null
  const data = { ...entity }
  const Renderer = data.renderer
  delete data.renderer
  return <Renderer {...data} />
}

private renderMultipleEntities = (entities: any[]) => {
  if (!entities || !entities.length) return null
  return entities.map(x => this.renderEntity(x))
}

render () {
  return <GameLoop style={styles.engine} onUpdate={this.onUpdate}>
    {this.renderEntity(this.ball)}
    {this.renderEntity(this.finishDetector)}
    {this.renderMultipleEntities(this.walls)}
    {this.renderMultipleEntities(this.arcs)}
  </GameLoop>
}

reyalpsirc avatar Apr 15 '20 11:04 reyalpsirc

Hi @reyalpsirc,

Did your Android device have low battery whilst you were using the GameEngine? I ask because I've hear that a number of factors could cause the JS frame ticks to go from 60fps (default I believe) to 30fps (conservative).

Also, where you doing anything else when using the GameEngine component? Did you have any heavy-logic systems running before each render loop? How many entities are you rendering? How many entities do you have altogether (including entities that don't need to be rendered)?

Your GameLoop code does pretty much exactly what the GameEngine does, so I'm curious to find the bottleneck..

bberak avatar Apr 16 '20 00:04 bberak

@bberak Nope, the device had full battery.

In my real scenario I do have more things like listening to the Accelerometer values but, the FPS drop happens even when I disabled that and created a GameEngine with empty entities and systems.

With the GameEngine and all my systems, I was getting like 10/15 FPS with my full system on debug mode while now I get around 25/30 FPS with that same system. One thing that also helped on this was that I only needed to update 2 entities (because all the others are static bodies) and so, I directly forced the update of those on the onUpdate method (using their refs that I created).

Anyway, and "empty" GameEngine should not drop to 30 FPS, specially if an empty GameLoop keeps steady on 60 FPS right?

reyalpsirc avatar Apr 16 '20 08:04 reyalpsirc

I also have low FPS on a real device (strong device), only when adding multiple objects to the game.

@reyalpsirc, can you explain how did you manage to fix it? without GameEngine, u still manage to do physics and staff?

melkayam92 avatar Apr 17 '20 11:04 melkayam92

@melkayam92 What I did for the physics was to put the Matter.Engine.update(this.engine, time.delta); inside the onUpdate function, where "this.engine" is the engine you created on the constructor.

Also, all the objects that use physics and are not static will need to be force Updated. That's why in my case I also have this: if(this.entityRefs['ball']) this.entityRefs['ball'].forceUpdate() inside the same onUpdate method.

This also means that I changed my renderEntity and renderMultipleEntities to this:

protected renderEntity = (entity) => {
  if (!this.entities[entityName]) throw new Error(`Entity not found: ${entityName}`)
  const data = { ...this.entities[entityName] }
  const Renderer = data.renderer
  delete data.renderer
  return <Renderer ref={(ref) => this.entityRefs[entityName] = ref} {...data} />
}

protected renderMultipleEntities = (groupEntityName) => {
  if (!this.entities[groupEntityName]) throw new Error(`Entity group not found: ${groupEntityName}`)
  this.entityRefs[groupEntityName] = {}
  return this.entities[groupEntityName].map((x, idx) => {
    if (!x) return null
    const data = { ...x }
    const Renderer = data.renderer
    delete data.renderer
    return <Renderer key={`${groupEntityName}_${idx}`} ref={(ref) => this.entityRefs[groupEntityName][idx] = ref} {...data} />
  })
}

Note also that I'm now storing all my entities data in this.entities and the refs in this.entityRefs Hope this helps you :)

reyalpsirc avatar Apr 17 '20 12:04 reyalpsirc

Thanks @reyalpsirc , i will give it a try!

melkayam92 avatar Apr 17 '20 15:04 melkayam92

Thanks for the info and help @reyalpsirc.

Quick question - were your renderers extending from Component or PureComponent?

Also, if you get a chance to run the Handbook project (https://github.com/bberak/react-native-game-engine-handbook) on your device - I'll be curious to see what performance you get (especially from some of the physics examples, and examples that spawn lots of entities).

bberak avatar Apr 18 '20 06:04 bberak

@bberak They are all PureComponent's

I tried the handbook project before but I was not able to run it's non-expo version. I tried to correct it but I just wanted to check it and so I went with the expo version.

I just used the expo version again now to check the FPS and I found out that on the menu, with the falling snow, has only around 22 FPS. Regarding the Rigidbodies example, it has around 30 FPS and once I added around 10/15 boxes, it was showing 22-25 FPS.

The device I tested this was a Nexus 5X

reyalpsirc avatar Apr 18 '20 20:04 reyalpsirc

Thanks for the info @reyalpsirc .. I'll head out and get a physical Android device to do some testing/debugging. I only had access to a Android simulator (which worked fine), but that cleary doesn't correlate to a physical device.

bberak avatar Apr 19 '20 02:04 bberak

Hello @bberak,

Did you have any luck with your testing on a real Android device?

I also keep getting some hanging performance issues when running my application on Android and this happend when I added around 100 different bodies and some sprites, so this could be the reason. But on the other hand on iOS it works great without any issues.

Any advice is appreciated, thank you 👍

lexengineer avatar Jul 14 '20 15:07 lexengineer

Hey @reyalpsirc and @oleksiikiselov,

I also experienced the frame drop when adding lots of bodies/sprites (using a very low-end Android device).

I've found that I can improve the Android performance a fair bit (about 60%-80% on my device) by overriding the default timer that the GameEngine uses to run the game loop.

Here is a an example of how to switch out the default timer: https://github.com/bberak/rnge-particle-playground/blob/72e21513e905170e502dbf89a7b994d8224d7d17/App.js#L15

And here is a link to the timer that I use (it tries to run the game loop at about 60fps): https://github.com/bberak/rnge-particle-playground/blob/master/src/utils/perf-timer.js

Let me know how that goes - hopefully it helps..

Also, I've never tried this, but it might help to try using the Hermes engine: https://reactnative.dev/docs/hermes

bberak avatar Jul 16 '20 06:07 bberak

hey @bberak, I'm experiencing the lags in android as well, i tried the timer fix but i haven't seen much improvement. I use MatterJs to handle entities positions. I have 10 entities scrolling down in the screen, basically round colored view, and when it goes out of the screen i translate the position to the top again. If i reduce the number of entities to 2 the performance is better but still not as good as on IOS. Thanks

medmo7 avatar Nov 10 '20 14:11 medmo7

I just added Hermes, and i made a release Android build on my phone, the result is comparable to what i get in a debug build on an IOS device.

medmo7 avatar Nov 10 '20 15:11 medmo7

@medmo7 How are you updating the position given by MatterJS? Are you using the component’s state or are you updating each required entity individually through a ref and a call to its forceUpdate method? Besides the moving entities, do you have any other entities?

reyalpsirc avatar Nov 10 '20 18:11 reyalpsirc

@reyalpsirc i use component props to update moving entities. I use the same comp for all the moving entities here is it:

import React  from 'react';
import {TouchableOpacity} from 'react-native';

export default function Tap(props) {
  const width = props.size[0];
  const height = props.size[1];
  const x = props.body.position.x - width / 2;
  const y = props.body.position.y - height / 2;

  const handlePress = () => {
    props.eventDispatch({
      type: 'TAP',
      body: props.body,
      category: props.category,
    });
  };
  const color =
    props.category === 'good'
      ? 'green'
      : props.category === 'bad'
      ? 'red'
      : 'gold';
  return (
    <TouchableOpacity
      onPress={handlePress}
      style={{
        position: 'absolute',
        left: x,
        top: y,
        width: width,
        height: height,
        borderRadius: 100,
        backgroundColor: color,
      }}
    />
  );
}

Besides the moving entities i have the physics entity which includes the MatterJs engine and world. I have also a spriteSheet looping in the scene, but i think it's not causing any issue because when i remove it the perf are the same. Thanks

medmo7 avatar Nov 11 '20 09:11 medmo7

@medmo7 Just a guess here but have you tried with a PureComponent class instead of a functional component?

reyalpsirc avatar Nov 11 '20 16:11 reyalpsirc

@reyalpsirc yes i did, but the positions of entities does not update in that case. Maybe i missed something.

medmo7 avatar Nov 11 '20 16:11 medmo7

@medmo7 in my case, I used the PureComponent for my entities and then on the PhysicsSystemUpdate method (the one where you call Matter.Engine.update) I accessed the ref of my ball (in my case it was a ball moving according to accelerometer) and forced an update there: if (this.ballRef) this.ballRef.forceUpdate().

reyalpsirc avatar Nov 11 '20 16:11 reyalpsirc

sorry how do you get access to the ball ref in the system file?

medmo7 avatar Nov 11 '20 16:11 medmo7

@medmo7 I don’t have the system on a separate file. In my case it is a method of my Main component and so, all I need to do is set the ref on the render method and use it directly on the system method.

reyalpsirc avatar Nov 11 '20 17:11 reyalpsirc

@medmo7 I think if you use PureComponent you will also need to provide a shouldComponentUpdate method. My guess is that your positions did not update because the body prop itself doesn't change from frame-to-frame (even though the body.position.x and body.position.y values may change. PureComponent will only do a shallow compare as far as I'm aware - so it won't trigger an update unless the body prop itself changes..

bberak avatar Nov 13 '20 02:11 bberak

@bberak you right, when providing shouldComponentUpdate the positions do update. I decided to change the gameplay from scrolling entities to just appearing and disappearing after an amount of time, this gives better perf result and i can use less entities.

medmo7 avatar Nov 13 '20 08:11 medmo7