Giter Site home page Giter Site logo

icecreamyou / mainloop.js Goto Github PK

View Code? Open in Web Editor NEW
513.0 27.0 54.0 1.41 MB

Provides a well-constructed main loop useful for JavaScript games and other animated or time-dependent applications.

License: MIT License

JavaScript 100.00%
mainloop javascript frame-rate games loop javascript-games time physics animation webgl

mainloop.js's Introduction

Build Status npm version

MainLoop.js provides a well-constructed main loop useful for JavaScript games and other animated or time-dependent applications.

The main loop is a core part of any application in which state changes over time. In games, it is typically responsible for computing physics and AI as well as drawing the result on the screen.

Main loops are difficult to write correctly due to timing issues. The vast majority of main loops found online are written incorrectly, resulting in applications that speed up or slow down depending on the frame rate. This can cause unfortunate behavior like characters running through walls or being unable to jump over obstacles. These main loops can also result in applications that are non-deterministic. This project solves these problems.

Get started

Installation

You can download the script normally, install it with npm (npm install mainloop.js), or install it with Bower (bower install mainloop). To include it on a page client-side without a module loader:

<!-- from a direct download or git clone -->
<script src="build/mainloop.min.js"></script>

<!-- from npm -->
<script src="node_modules/mainloop.js/build/mainloop.min.js"></script>

<!-- from Bower -->
<script src="bower_components/mainloop.js/build/mainloop.min.js"></script>

<!-- via CDN -->
<script src="https://cdn.jsdelivr.net/npm/mainloop.js@latest/build/mainloop.min.js"></script>

You then have access to the MainLoop global.

MainLoop.js is also compatible with CommonJS (e.g. with node.js or browserify) and AMD (e.g. with RequireJS). This means that if you are using a module loader or want to use MainLoop server-side you can call require('mainloop.js') to get the MainLoop object or include 'mainloop.js' in the dependencies you pass to a define() call.

For TypeScript users, there are typings available. Install them with npm install --save-dev @types/mainloop.js.

Usage

MainLoop works by running functions you define every time the browser is ready to update the screen (up to about 60 times per second on most monitors). There are four such functions, all of which are optional. You can set them using the following methods:

  • MainLoop.setBegin(): the begin function runs at the beginning of each frame and is typically used to process input.
  • MainLoop.setUpdate(): the update function runs zero or more times per frame depending on the frame rate. It is used to compute anything affected by time - typically physics and AI movements.
  • MainLoop.setDraw(): the draw function should update the screen, usually by changing the DOM or painting a canvas.
  • MainLoop.setEnd(): the end function runs at the end of each frame and is typically used for cleanup tasks such as adjusting the visual quality based on the frame rate.

The update function receives a delta parameter which holds the amount of time in milliseconds that should be simulated. This should be used to calculate movement. For example, if an object obj has an x-axis velocity of 100 units per second (0.1 units per millisecond), the update function might look like this:

function update(delta) {
    obj.x += 0.1 * delta;
}

This structure will ensure that your application behaves regardless of the frame rate.

To start the application, simply call MainLoop.start(). For example, to start the application for the first time, you might write:

MainLoop.setUpdate(update).setDraw(draw).start();

You can call MainLoop.stop() to stop the application.

For more detail about the API, check out the documentation. You can also check out a usage example (source code).

Notes

This project is MIT-licensed.

Compatible with all modern browsers (IE9+) including mobile browsers, as well as node.js. There are no dependencies.

Contributions are welcome. To get started contributing, run npm install in the project's directory, then run grunt before submitting a pull request to update the minified script and the docs as well as to perform a style check.

The library is < 1KB minified and gzipped.

Isaac Sukin (@IceCreamYou) is the author of this project. I'd love to hear about what you make! Special thanks to Ian Langworth for reviewing a version of this I wrote for my book about making 3D browser games and for some tips about web workers.

mainloop.js's People

Contributors

dependabot[bot] avatar icecreamyou avatar jamesplease avatar lukasdrgon avatar thomwright avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mainloop.js's Issues

Aw, Snap!

After a couple seconds when i am using this library chrome says: "Aw, Snap!".

I'm using pixijs. I'm just making some circles to move on the screen. The fps also lowers to under 18 fps.

update() function always called with the same parametervalue

Hello,
Im new to this topic and read your mainLoop article at:
https://isaacsukin.com/news/2015/01/detailed-explanation-javascript-game-loops-and-timing#fps-control

I really found it helpful and want to try things out with this.
However I was wondering why the update(simulationTimestep) function always gets called with the same value.
Earlier in your article we passed delta as parameter, where it sent the elapsed time since the last update.
Later in the article you said it has to be called with the same delta since we would get wierd rounding errors, which leads to moving through objects.

So thinking about it is there any use for this "delta"? The calculated velocity/movement will always be the same amount anyways.

Thanks in advance.

Allow specifying a different requestAnimationFrame function

This library currently uses window.requestAnimationFrame if available and falls back to setTimeout otherwise. The upcoming WebVR API will use display.requestAnimationFrame to schedule frames on attached VR displays. This library should support such loops.

Making loop run at an accelerated rate

Hi,

I've been trying to force the loop to execute more quickly so that I can test my game code at a faster rate (game runs twice as fast for example). I've tried updating the simulated timestep and/or max FPS but I think there's a throttle built into MainLoop which is actively preventing me from running too quickly.

Do you have any tips on what I can do?

Many thanks!
Andy

Example that uses setBegin?

Hi @IceCreamYou ! I know it's been a couple of years since you've worked on this library, so no worries at all if you've moved on and don't have the time and/or interest to triage issues.

I love the example that ships with MainLoop. It really shows the value of using this library over many other solutions out there. I have a question for ya': do you happen to have another example handy that uses setBegin to process user input? It could be cool to demonstrate to folks best practices for how to use that method.

Thanks for reading!

Calling MainLoop.stop() from within animate() doesn't work

Basically, MainLoop.stop() stops the loop by canceling the frame referenced by the rafHandle variable. However, inside of animate() (which calls the begin/draw/update/end functions), rafHandle points to the current frame, so canceling it doesn't do anything. Afterwards, requestAnimationFrame() still gets called again at the end of animate(), setting a new rafHandle for the next frame.

The solution to this is to simply move the requestAnimationFrame() call from the bottom of animate() to the top. As far as I can tell, this has no negative effects. It has one additional positive effect, which is that if we have to fall back to the setTimeout()-based polyfill for requestAnimationFrame(), the frames will be called slightly closer to on-time.

Update library to Node Module

Since last commit was 2 years ago is a nice idea update the library to fully work with the node module system (a.k.a. export default/import), once this feature is more mature now.

why such a big dip every few seconds in fps?

Hello

I tried running your demo version of this loop on a 144hz monitor and occasionally am getting 100+ fps even if the slider is set to 60.

Am I misunderstanding what this will do for you or is this a bug?

This is roughly what I see happening:

image

publish new version to npm registry

Hi!

I see you added "main" in package.json about 2 months ago, but you have not published a new package to the npm registry.

So after doing "npm install mainloop.js" I still had to add main to use your lib.

Could you please publish a newer version of this package to the npm registry?

Thanks.

stop() on setTimeout fallback.

I haven't tested this yet personally but I noticed in stop() that you call cancelAnimationFrame(rafHandle). If the RAF polyfill falls back to setTimeout, wouldn't stop() fail to actually stop the loop from continuing since you would need to use clearTimeout() instead?

Remove "global" library state

This library only supports one instance per page due to the way it is written with global library state. A more idiomatic approach would be to expose a constructor that makes a new MainLoop. For instance,

var mainLoop = new MainLoop(options);

mainLoop.setEnd(cb);

// You can easily make another one
var mainLoopTwo = new MainLoop(options);

Given how well-organized the code is, I think it'd be relatively simple to refactor it to use this system instead.

If this is something you're interested in, let me know and I'd be happy to put together a PR for you to review.

Where is the good place to have Websocket send() calls with the library?

Hello!

Your article on game time step helped me so much when I was making a small game. Thank you for the article and this awesome library!

I am trying to make a multiplayer game with Websocket and I'm not sure where would be the ideal place to put the socket.send() calls. The game updates at fixed time step, and draws at 60fps. I would like to send the player inputs to the authoritative server at 30hz instead of 60hz. Is this possible to do with this library?

Thank you for your help!

bower install MainLoop doesn't work

Minor nitpick, but "bower install MainLoop" doesn't actually work (at least, not in git bash) but..

"bower install mainloop" DOES work. Is this an issue affecting everyone or just git bash users?

setInterval/setTimeout vs requestAnimationFrame?

Just trying to understand the approach a bit better before porting it-

Would it work to call update(simulationTimestep) and updating all the related variables inside setInterval/setTimeout - even if render() is scheduled with rAF? Talking about browsers only and assuming that the callback can call performance.now()

In other words, I'm trying to understand why physics updates are also done in rAF which, as you've mentioned is tied to the display rate and can be delayed when switching tabs etc.

Thanks!

Document or improve the fact that the `draw` callback gets called before the loop is run

Currently, when MainLoop.start() is called, draw() is called to render the initial state before any updates occur. This behavior could lead to users writing buggy code based on the reasonable expectation that begin() and end() are called before and after draw(), respectively.

I think it would be fine to call begin() and end() before and after draw() when MainLoop.start() is called, but this might be considered a backwards-incompatible change. The alternative would be to document this behavior so that people know to manually call begin() and end() if needed.

How I can avoid calling setEnd 60 times per second?

I would like to only execute "setEnd" 20 times per second, because updating updateGamestate 60 times per second over the network would be too much expensive.

This code is kind of working for me but it looks like it gives performance issues.

const nus = 20 // Number of updates per second - 1 min, 60 max
let emit = true
setInterval(() => (emit = true), 1000 / nus)

mainloop.setEnd(() => {
	if (emit) {
		io.emit('gameState', updateGamestate(bodies))
		emit = false
	}
})

There is a proper way to limit the number of calls to setEnd?

port to Rust / WASM

would love to have this for rust/wasm projects...

If I go for it, how should I handle the license? (happy for it to be MIT too)

of course it would mostly be copy/paste and I want to give you full credit for all the hard work you put into it - but Rust/WASM has its own set of problems that need some original solutions too.

Using a class method as function for "Update" or "Draw"

Hello !

I'm having a problem with the Update function.
Let's say I have a class named "Game" which has a method named "update"
Normally, I could do this :

let game = new Game();
game.update();

and the method could be like this :
update() {
this.objects.doStuff();
}

But when I try to call the method with the Update call, I can't access "this"
I get the following error : this is undefined

Is it due to the plugin implementation or is this simple javascript behaviour that I'm not aware of ?

Thank you very much.

Designing a networked (Node.js) main loop for multiplayer capability

Hey Isaac!

Thanks a lot for this awesome repository. How would you suggest going about a Node.js implementation? I can't quite figure out the best way to do this.

I'm currently running an interval on the server-side, which streams out data to the client-side, which will then render the game (as for now). But as you probably know, this is not the best solution.

Basically I need to figure out a way to stream updates from server-side to client-side, and then render it in the most efficient way.

Any input is much appreciated!

`update` step implementation question

Currently the update function is called multiple times in a row:

numUpdateSteps = 0;
while (frameDelta >= simulationTimestep) {
    update(simulationTimestep);
    frameDelta -= simulationTimestep;

    if (++numUpdateSteps >= 240) {
        panic = true;
        break;
    }
}

Since it uses time delta anyway, why can't it be condensed into the single update call based on how much time has passed? It seems that this way would require only one update call for the loop to catch up instead of several at times.

if ((simulationTimestep / frameDelta) >= 240) {
    panic = true;
}

update(frameDelta);

draw(frameDelta / simulationTimestep);

Deep call stack

This runs great, but the recursive call to requestAnimation causes the call stack to become pretty deep. I'm only using the loop for update(), so maybe the issue is with how I integrated it?

I've integrated this into my N64 javascript emu at http://1964js.com.

FPS Calculation

There might be some minor issues with the FPS calculation:

  1. FPS < 1 is currently not possible, but this might happen in test environments.
    Proposed solution: Update the EMA FPS value n times in each loop with the value frameCount / n, where n is the number of seconds passed since the last update.
  2. FPS update intervals are currently a little longer than 1 second, because:
    timestamp > lastFpsUpdate + 1000.
    So currently it does not calculate:
    frames / second
    but:
    frames / (second + time until next frame starts).
    This could be corrected this with:
    while (lastFpsUpdate < timestamp - 1000) lastFpsUpdate += 1000;
    instead of:
    lastFpsUpdate = timestamp;

mainloop server-side not working because of "window"

Hello, the documentation says that it can be used in Node.js with require('mainloop')
But when I tested, it simply isn't possible because of the use of "window', which obviously isn't available server-side...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.