bberak / react-native-game-engine

A lightweight Game Engine for React Native 🕹⚡🎮
MIT License
2.88k stars 172 forks source link

Android performance #18

Open lukechatton opened 5 years ago

lukechatton commented 5 years ago

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.

bberak commented 5 years ago

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).

lukechatton commented 5 years ago

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 commented 5 years ago

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.

bberak commented 5 years ago

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 commented 5 years ago

Hi @lukechatton

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

lukechatton commented 5 years ago

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. 👍

bberak commented 5 years ago

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

Cheers!

reyalpsirc commented 4 years ago

@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 commented 4 years ago

@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>
}
bberak commented 4 years ago

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..

reyalpsirc commented 4 years ago

@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?

melkayam92 commented 4 years ago

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?

reyalpsirc commented 4 years ago

@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 :)

melkayam92 commented 4 years ago

Thanks @reyalpsirc , i will give it a try!

bberak commented 4 years ago

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).

reyalpsirc commented 4 years ago

@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

bberak commented 4 years ago

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.

lexengineer commented 4 years ago

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 👍

bberak commented 4 years ago

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

medmo7 commented 3 years ago

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 commented 3 years ago

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.

reyalpsirc commented 3 years ago

@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?

medmo7 commented 3 years ago

@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

reyalpsirc commented 3 years ago

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

medmo7 commented 3 years ago

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

reyalpsirc commented 3 years ago

@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().

medmo7 commented 3 years ago

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

reyalpsirc commented 3 years ago

@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.

bberak commented 3 years ago

@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..

medmo7 commented 3 years ago

@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.

bembem1011 commented 3 years ago

https://github.com/bberak/react-native-game-engine-handbook I clone and build, install above github on my device. It run so slow (added hermes), not smoothly like running on ios :( my device is samsung S7 android 8

rafaelmaeuer commented 3 years ago

When building my game-app with rn-game-engine I always noticed a bad performance on Android. I then did some performance measurements between iPhones and Android phones with Geekbench, figuring out that iPhones have much higher single core scores compared to Android phones. Android Devices seem to back more on multi-core power (most of the time 8 cores) with a low frequency while iPhones count on less cores (2-4 cores) but high frequency.

As RN doesn't make use of multiple cores due to the JS-Bridge bottleneck this explanation seemed always useful to me, why iOS performance is so much better than Android. But today and after reading this issue, I did some testing with an iPhone 6S, a Samsung Galaxy S7 Edge and a Samsung Galaxy A6 with the rn-game-engine-handbook and Geekbench 5:

IMG_8896

IMG_8901

So the Single-Core Scores on Android aren't reflecting the performance of rn-game-engine on Android devices. I will try to upgrade my game-app to hermes in hope of some small performance improvements, but otherwise currently the game isn't usable on these Android devices at all... @bberak any thoughts on this?

smerch88 commented 6 months ago

@rafaelmaeuer any updates on performance?

rafaelmaeuer commented 3 months ago

Nope - I did not test this for a long time...