การควบคุม fps ด้วย requestAnimationFrame?


146

ดูเหมือนว่าrequestAnimationFrameจะเป็นวิธีการทำให้สิ่งต่างๆเคลื่อนไหวโดยพฤตินัยแล้ว มันทำงานได้ดีสำหรับฉันเป็นส่วนใหญ่ แต่ตอนนี้ฉันกำลังพยายามทำแอนิเมชั่นแคนวาสและฉันสงสัยว่า: มีวิธีใดบ้างที่จะทำให้แน่ใจว่ามันทำงานที่เฟรมต่อวินาที? ฉันเข้าใจว่าจุดประสงค์ของ RAF คือเพื่อให้ภาพเคลื่อนไหวที่ราบรื่นสม่ำเสมอและฉันอาจเสี่ยงต่อการทำให้แอนิเมชั่นขาด ๆ หาย ๆ แต่ตอนนี้ดูเหมือนว่าจะทำงานด้วยความเร็วที่แตกต่างกันอย่างมากโดยพลการและฉันสงสัยว่ามีวิธีต่อสู้หรือไม่ อย่างนั้น

ฉันจะใช้setIntervalแต่ฉันต้องการการเพิ่มประสิทธิภาพที่ rAF เสนอ (โดยเฉพาะหยุดโดยอัตโนมัติเมื่อแท็บอยู่ในโฟกัส)

ในกรณีที่มีคนต้องการดูรหัสของฉันมันค่อนข้างมาก:

animateFlash: function() {
    ctx_fg.clearRect(0,0,canvasWidth,canvasHeight);
    ctx_fg.fillStyle = 'rgba(177,39,116,1)';
    ctx_fg.strokeStyle = 'none';
    ctx_fg.beginPath();
    for(var i in nodes) {
        nodes[i].drawFlash();
    }
    ctx_fg.fill();
    ctx_fg.closePath();
    var instance = this;
    var rafID = requestAnimationFrame(function(){
        instance.animateFlash();
    })

    var unfinishedNodes = nodes.filter(function(elem){
        return elem.timer < timerMax;
    });

    if(unfinishedNodes.length === 0) {
        console.log("done");
        cancelAnimationFrame(rafID);
        instance.animate();
    }
}

โดยที่ Node.drawFlash () เป็นเพียงโค้ดบางส่วนที่กำหนดรัศมีโดยอิงจากตัวแปรตัวนับจากนั้นจึงวาดวงกลม


1
ภาพเคลื่อนไหวของคุณล่าช้าหรือไม่ ฉันคิดว่าข้อได้เปรียบที่ใหญ่ที่สุดrequestAnimationFrameคือ (ตามชื่อที่แนะนำ) ขอเฟรมแอนิเมชั่นเมื่อจำเป็นเท่านั้น สมมติว่าคุณแสดงผ้าใบสีดำแบบคงที่คุณควรได้ 0 fps เพราะไม่จำเป็นต้องใช้เฟรมใหม่ แต่ถ้าคุณกำลังแสดงภาพเคลื่อนไหวที่ต้องใช้ 60fps คุณก็ควรได้รับเช่นกัน rAFเพียงแค่อนุญาตให้ "ข้าม" เฟรมที่ไร้ประโยชน์แล้วบันทึก CPU
maxdec

setInterval ไม่ทำงานในแท็บที่ไม่ใช้งานด้วย
ViliusL

รหัสนี้ทำงานแตกต่างกันบนจอแสดงผล 90hz เทียบกับจอแสดงผล 60hz เทียบกับจอแสดงผล 144hz
manthrax

คำตอบ:


201

วิธีการเค้น requestAnimationFrame ไปยังอัตราเฟรมที่เฉพาะเจาะจง

สาธิตการควบคุมที่ 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

วิธีนี้ใช้ได้ผลโดยการทดสอบเวลาที่ผ่านไปนับตั้งแต่ดำเนินการวนรอบเฟรมสุดท้าย

โค้ดรูปวาดของคุณจะทำงานเฉพาะเมื่อช่วงเวลา FPS ที่คุณกำหนดผ่านไป

ส่วนแรกของโค้ดจะตั้งค่าตัวแปรบางตัวที่ใช้ในการคำนวณเวลาที่ผ่านไป

var stop = false;
var frameCount = 0;
var $results = $("#results");
var fps, fpsInterval, startTime, now, then, elapsed;


// initialize the timer variables and start the animation

function startAnimating(fps) {
    fpsInterval = 1000 / fps;
    then = Date.now();
    startTime = then;
    animate();
}

และรหัสนี้คือลูป requestAnimationFrame จริงซึ่งดึงที่ FPS ที่คุณระบุ

// the animation loop calculates time elapsed since the last loop
// and only draws if your specified fps interval is achieved

function animate() {

    // request another frame

    requestAnimationFrame(animate);

    // calc elapsed time since last loop

    now = Date.now();
    elapsed = now - then;

    // if enough time has elapsed, draw the next frame

    if (elapsed > fpsInterval) {

        // Get ready for next frame by setting then=now, but also adjust for your
        // specified fpsInterval not being a multiple of RAF's interval (16.7ms)
        then = now - (elapsed % fpsInterval);

        // Put your drawing code here

    }
}

17
การสาธิตที่ดี - ควรได้รับการยอมรับ ที่นี่แยกซอของคุณเพื่อสาธิตการใช้ window.performance.now () แทน Date.now () สิ่งนี้เข้ากันได้ดีกับการประทับเวลาความละเอียดสูงที่ rAF ได้รับแล้วดังนั้นจึงไม่จำเป็นต้องเรียก Date.now () ภายในการโทรกลับ: jsfiddle.net/chicagogrooves/nRpVD/2
Dean Radcliffe

2
ขอขอบคุณสำหรับลิงก์ที่อัปเดตโดยใช้คุณลักษณะการประทับเวลา rAF ใหม่ การประทับเวลา rAF ใหม่เพิ่มโครงสร้างพื้นฐานที่เป็นประโยชน์และยังแม่นยำกว่า Date.now
markE

13
นี่เป็นตัวอย่างที่ดีมากซึ่งเป็นแรงบันดาลใจให้ฉันทำ ( JSFiddle ) ของตัวเอง ความแตกต่างที่สำคัญคือการใช้ rAF (เช่นการสาธิตของ Dean) แทน Date การเพิ่มการควบคุมเพื่อปรับอัตราเฟรมเป้าหมายแบบไดนามิกการสุ่มตัวอย่างเฟรมเรตในช่วงเวลาที่แยกจากภาพเคลื่อนไหวและการเพิ่มกราฟของเฟรมเรตในอดีต
tavnab

นี่เป็นวิธีการแก้ปัญหาที่ชาญฉลาดปัญหาเดียวคือมันสร้างค่าใช้จ่ายเพิ่มเติมใน RAF และอาจส่งผลเสียต่ออัตราเฟรมจริงเนื่องจากการจัดการข้อมูลทั้งหมดที่เกิดขึ้นภายใน RAF หากต้องการหลีกเลี่ยงสิ่งนี้ให้จัดการข้อมูลใน setInterval แยกกันถ้าเป็นไปได้ในผู้ปฏิบัติงานเว็บเพื่อให้มีเธรดเป็นของตัวเอง ตามหลักการแล้ว RAF ควรอัปเดตเฉพาะกราฟิกและอ่านวัตถุ js ที่มีข้อมูลปัจจุบัน การจัดการข้อมูลควรทำนอก RAF และข้อมูลใหม่ที่อยู่ในออบเจ็กต์เพื่อให้ RAF โทรกลับเพื่ออ่าน
jdmayfield

1
สิ่งที่คุณควบคุมได้ก็คือเมื่อคุณกำลังจะข้ามเฟรม จอภาพ 60 fps จะดึงที่ช่วง 16ms เสมอ ตัวอย่างเช่นหากคุณต้องการให้เกมของคุณทำงานที่ 50fps คุณต้องการข้ามทุกๆเฟรมที่ 6 คุณตรวจสอบว่าเวลาผ่านไป 20 มิลลิวินาที (1000/50) แล้วและยังไม่ (ผ่านไปเพียง 16 มิลลิวินาที) ดังนั้นคุณจึงข้ามเฟรมจากนั้นเฟรมถัดไป 32 มิลลิวินาทีผ่านไปนับตั้งแต่ที่คุณวาดดังนั้นคุณจึงวาดและรีเซ็ต แต่คุณจะข้ามครึ่งเฟรมและวิ่งที่ 30fps ดังนั้นเมื่อคุณรีเซ็ตคุณจำได้ว่าคุณรอ 12ms เป็นเวลานานเกินไป ดังนั้นเฟรมถัดไปอีก 16ms จะผ่านไป แต่คุณนับเป็น 16 + 12 = 28ms ดังนั้นคุณจึงวาดอีกครั้งและรอนานเกินไป 8ms
Curtis

50

อัปเดต 2016/6

ปัญหาในการควบคุมอัตราเฟรมคือหน้าจอมีอัตราการอัปเดตคงที่โดยทั่วไปคือ 60 FPS

หากเราต้องการ 24 FPS เราจะไม่ได้ 24 fps ที่แท้จริงบนหน้าจอเราสามารถตั้งเวลาได้เช่นนี้ แต่ไม่แสดงเนื่องจากจอภาพสามารถแสดงเฟรมที่ซิงค์ได้ที่ 15 fps, 30 fps หรือ 60 fps เท่านั้น (จอภาพบางจอ 120 fps ).

อย่างไรก็ตามเพื่อจุดประสงค์ด้านเวลาเราสามารถคำนวณและอัปเดตเมื่อเป็นไปได้

คุณสามารถสร้างตรรกะทั้งหมดสำหรับการควบคุมอัตราเฟรมโดยการห่อหุ้มการคำนวณและการเรียกกลับลงในวัตถุ:

function FpsCtrl(fps, callback) {

    var delay = 1000 / fps,                               // calc. time per frame
        time = null,                                      // start time
        frame = -1,                                       // frame count
        tref;                                             // rAF time reference

    function loop(timestamp) {
        if (time === null) time = timestamp;              // init start time
        var seg = Math.floor((timestamp - time) / delay); // calc frame no.
        if (seg > frame) {                                // moved to next frame?
            frame = seg;                                  // update
            callback({                                    // callback function
                time: timestamp,
                frame: frame
            })
        }
        tref = requestAnimationFrame(loop)
    }
}

จากนั้นเพิ่มคอนโทรลเลอร์และรหัสการกำหนดค่า:

// play status
this.isPlaying = false;

// set frame-rate
this.frameRate = function(newfps) {
    if (!arguments.length) return fps;
    fps = newfps;
    delay = 1000 / fps;
    frame = -1;
    time = null;
};

// enable starting/pausing of the object
this.start = function() {
    if (!this.isPlaying) {
        this.isPlaying = true;
        tref = requestAnimationFrame(loop);
    }
};

this.pause = function() {
    if (this.isPlaying) {
        cancelAnimationFrame(tref);
        this.isPlaying = false;
        time = null;
        frame = -1;
    }
};

การใช้งาน

มันกลายเป็นเรื่องง่ายมาก - ตอนนี้สิ่งที่เราต้องทำก็คือการสร้างอินสแตนซ์โดยตั้งค่าฟังก์ชันเรียกกลับและอัตราเฟรมที่ต้องการดังนี้:

var fc = new FpsCtrl(24, function(e) {
     // render each frame here
  });

จากนั้นเริ่มต้น (ซึ่งอาจเป็นพฤติกรรมเริ่มต้นหากต้องการ):

fc.start();

เพียงเท่านี้ตรรกะทั้งหมดจะถูกจัดการภายใน

การสาธิต

var ctx = c.getContext("2d"), pTime = 0, mTime = 0, x = 0;
ctx.font = "20px sans-serif";

// update canvas with some information and animation
var fps = new FpsCtrl(12, function(e) {
	ctx.clearRect(0, 0, c.width, c.height);
	ctx.fillText("FPS: " + fps.frameRate() + 
                 " Frame: " + e.frame + 
                 " Time: " + (e.time - pTime).toFixed(1), 4, 30);
	pTime = e.time;
	var x = (pTime - mTime) * 0.1;
	if (x > c.width) mTime = pTime;
	ctx.fillRect(x, 50, 10, 10)
})

// start the loop
fps.start();

// UI
bState.onclick = function() {
	fps.isPlaying ? fps.pause() : fps.start();
};

sFPS.onchange = function() {
	fps.frameRate(+this.value)
};

function FpsCtrl(fps, callback) {

	var	delay = 1000 / fps,
		time = null,
		frame = -1,
		tref;

	function loop(timestamp) {
		if (time === null) time = timestamp;
		var seg = Math.floor((timestamp - time) / delay);
		if (seg > frame) {
			frame = seg;
			callback({
				time: timestamp,
				frame: frame
			})
		}
		tref = requestAnimationFrame(loop)
	}

	this.isPlaying = false;
	
	this.frameRate = function(newfps) {
		if (!arguments.length) return fps;
		fps = newfps;
		delay = 1000 / fps;
		frame = -1;
		time = null;
	};
	
	this.start = function() {
		if (!this.isPlaying) {
			this.isPlaying = true;
			tref = requestAnimationFrame(loop);
		}
	};
	
	this.pause = function() {
		if (this.isPlaying) {
			cancelAnimationFrame(tref);
			this.isPlaying = false;
			time = null;
			frame = -1;
		}
	};
}
body {font:16px sans-serif}
<label>Framerate: <select id=sFPS>
	<option>12</option>
	<option>15</option>
	<option>24</option>
	<option>25</option>
	<option>29.97</option>
	<option>30</option>
	<option>60</option>
</select></label><br>
<canvas id=c height=60></canvas><br>
<button id=bState>Start/Stop</button>

คำตอบเก่า

จุดประสงค์หลักrequestAnimationFrameคือเพื่อซิงค์การอัปเดตกับอัตราการรีเฟรชของจอภาพ สิ่งนี้จะทำให้คุณต้องเคลื่อนไหวที่ FPS ของจอภาพหรือปัจจัยของมัน (เช่น 60, 30, 15 FPS สำหรับอัตราการรีเฟรชทั่วไปที่ 60 Hz)

หากคุณต้องการ FPS ตามอำเภอใจมากขึ้นไม่มีจุดใดที่จะใช้ rAF เนื่องจากอัตราเฟรมจะไม่ตรงกับความถี่ในการอัปเดตของจอภาพอีกต่อไป (เพียงแค่เฟรมตรงนี้และตรงนั้น) ซึ่งไม่สามารถให้ภาพเคลื่อนไหวที่ราบรื่นแก่คุณได้ (เช่นเดียวกับการกำหนดเวลาเฟรมใหม่ทั้งหมด ) และคุณสามารถใช้setTimeoutหรือใช้setIntervalแทนได้เช่นกัน

นี่เป็นปัญหาที่รู้จักกันดีในอุตสาหกรรมวิดีโอระดับมืออาชีพเมื่อคุณต้องการเล่นวิดีโอที่ FPS อื่นจากนั้นอุปกรณ์จะแสดงการรีเฟรชที่ มีการใช้เทคนิคหลายอย่างเช่นการผสมเฟรมและการกำหนดเวลาใหม่ที่ซับซ้อนในการสร้างเฟรมขั้นกลางใหม่โดยอาศัยเวกเตอร์การเคลื่อนไหว แต่เทคนิคเหล่านี้ใช้ผ้าใบไม่ได้และผลลัพธ์จะเป็นวิดีโอที่กระตุกเสมอ

var FPS = 24;  /// "silver screen"
var isPlaying = true;

function loop() {
    if (isPlaying) setTimeout(loop, 1000 / FPS);

    ... code for frame here
}

เหตุผลที่เราวางsetTimeout อันดับแรก (และทำไมบางตำแหน่งrAFก่อนเมื่อใช้การเติมโพลี) คือสิ่งนี้จะแม่นยำกว่าเนื่องจากsetTimeoutจะจัดคิวเหตุการณ์ทันทีเมื่อลูปเริ่มต้นดังนั้นไม่ว่าโค้ดที่เหลือจะใช้เวลาเท่าใด (หากไม่เกินช่วงหมดเวลา) การเรียกถัดไปจะอยู่ในช่วงเวลาที่แสดง (สำหรับ rAF บริสุทธิ์สิ่งนี้ไม่จำเป็นเนื่องจาก rAF จะพยายามข้ามไปยังเฟรมถัดไปไม่ว่าในกรณีใด ๆ ก็ตาม)

setIntervalนอกจากนี้ยังมีมูลค่าที่จะทราบว่าการวางมันเป็นครั้งแรกนอกจากนี้ยังจะมีความเสี่ยงสายซ้อนเช่นเดียวกับ setIntervalอาจจะแม่นยำกว่าเล็กน้อยสำหรับการใช้งานนี้

และคุณสามารถใช้setIntervalแทนนอกลูปเพื่อทำเช่นเดียวกัน

var FPS = 29.97;   /// NTSC
var rememberMe = setInterval(loop, 1000 / FPS);

function loop() {

    ... code for frame here
}

และเพื่อหยุดการวนซ้ำ:

clearInterval(rememberMe);

เพื่อลดอัตราเฟรมเมื่อแท็บเบลอคุณสามารถเพิ่มปัจจัยดังนี้:

var isFocus = 1;
var FPS = 25;

function loop() {
    setTimeout(loop, 1000 / (isFocus * FPS)); /// note the change here

    ... code for frame here
}

window.onblur = function() {
    isFocus = 0.5; /// reduce FPS to half   
}

window.onfocus = function() {
    isFocus = 1; /// full FPS
}

ด้วยวิธีนี้คุณสามารถลด FPS เป็น 1/4 เป็นต้น


4
ในบางกรณีคุณไม่ได้พยายามจับคู่อัตราเฟรมของจอภาพ แต่ในลำดับภาพเช่นวางเฟรม คำอธิบายที่ยอดเยี่ยม btw
sidonaldson

3
หนึ่งในเหตุผลที่ใหญ่ที่สุดในการเค้นด้วย requestAnimationFrame คือการจัดเรียงการทำงานของโค้ดบางอย่างกับเฟรมแอนิเมชั่นของเบราว์เซอร์ สิ่งต่างๆจะทำงานได้ราบรื่นขึ้นมากโดยเฉพาะอย่างยิ่งถ้าคุณใช้ตรรกะบางอย่างกับข้อมูลทุกเฟรมเช่นด้วยโปรแกรมสร้างภาพเพลง
Chris Dolphin

4
สิ่งนี้ไม่ดีเนื่องจากการใช้งานหลักrequestAnimationFrameคือการซิงโครไนซ์การดำเนินการ DOM (อ่าน / เขียน) ดังนั้นการไม่ใช้งานจะส่งผลเสียต่อประสิทธิภาพเมื่อเข้าถึง DOM เนื่องจากการดำเนินการจะไม่ถูกจัดคิวให้ดำเนินการร่วมกันและจะบังคับให้ทาสีเค้าโครงใหม่โดยไม่จำเป็น
vsync

1
ไม่มีความเสี่ยงที่จะเกิด "การโทรซ้อนกัน" เนื่องจาก JavaScript เรียกใช้เธรดเดียวและไม่มีเหตุการณ์การหมดเวลาใด ๆ เกิดขึ้นในขณะที่โค้ดของคุณกำลังทำงาน ดังนั้นหากฟังก์ชันใช้เวลานานกว่าการหมดเวลาฟังก์ชันจะทำงานเกือบทุกเวลาโดยเร็วที่สุดในขณะที่เบราว์เซอร์จะยังคงทำการวาดใหม่และเรียกใช้การหมดเวลาอื่น ๆ ระหว่างการโทร
dronus

ฉันทราบว่าคุณระบุว่าการรีเฟรชเพจไม่สามารถอัปเดตได้เร็วกว่าขีด จำกัด fps บนจอแสดงผล อย่างไรก็ตามเป็นไปได้ไหมที่จะรีเฟรชให้เร็วขึ้นโดยการเรียกใช้การแสดงซ้ำหน้า ในทางกลับกันเป็นไปได้ไหมที่จะไม่สังเกตเห็นการแสดงซ้ำหลายหน้าหากทำเร็วกว่าอัตรา fps ดั้งเดิม
Travis J

42

ฉันขอแนะนำให้ตัดสายของคุณไปrequestAnimationFrameที่setTimeout:

const fps = 25;
function animate() {
  // perform some animation task here

  setTimeout(() => {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}
animate();

คุณต้องโทรrequestAnimationFrameจากภายในsetTimeoutแทนที่จะใช้วิธีอื่นเนื่องจากrequestAnimationFrameกำหนดเวลาให้ฟังก์ชันของคุณทำงานก่อนการทาสีใหม่ครั้งต่อไปและหากคุณชะลอการอัปเดตเพิ่มเติมโดยใช้setTimeoutคุณจะพลาดช่วงเวลานั้นไป อย่างไรก็ตามการทำย้อนกลับเป็นเรื่องที่ดีเนื่องจากคุณเพียงแค่รอสักครู่ก่อนที่จะร้องขอ


1
ดูเหมือนว่าจะใช้งานได้จริงในการรักษาเฟรมเรตลงและไม่ทำให้ CPU ของฉันสุก และมันง่ายมาก ไชโย!
phocks

นี่เป็นวิธีที่ดีและเรียบง่ายสำหรับภาพเคลื่อนไหวที่มีน้ำหนักเบา มันไม่ได้รับการซิงค์เล็กน้อยอย่างน้อยก็ในอุปกรณ์บางอย่าง ฉันใช้เทคนิคนี้กับหนึ่งในเครื่องยนต์เดิมของฉัน มันทำงานได้ดีจนกว่าสิ่งต่างๆจะซับซ้อน ปัญหาที่ใหญ่ที่สุดคือเมื่อเชื่อมต่อกับเซ็นเซอร์การปรับทิศทางมันอาจจะล้าหลังหรือน่ากลัว ต่อมาฉันพบว่าใช้ setInterval แยกและการสื่อสารการอัปเดตระหว่างเซ็นเซอร์เฟรม setInterval และเฟรม RAF ผ่านคุณสมบัติของวัตถุทำให้เซ็นเซอร์และ RAF ทำงานแบบเรียลไทม์ในขณะที่เวลาในการเคลื่อนไหวสามารถควบคุมผ่านการอัปเดตคุณสมบัติจาก setInterval
jdmayfield

ตอบดีที่สุด! ขอบคุณ;)
538ROMEO

1
จอภาพของฉันคือ 60 FPS ถ้าฉันตั้งค่า var fps = 60 ฉันจะได้รับประมาณ 50 FPS โดยใช้รหัสนี้ ฉันต้องการทำให้ช้าลงเป็น 60 เพราะบางคนมีจอภาพ 120 FPS แต่ฉันไม่ต้องการให้กระทบกับคนอื่น นี่เป็นเรื่องยากอย่างน่าประหลาดใจ
Curtis

สาเหตุที่คุณได้ FPS ต่ำกว่าที่คาดไว้เนื่องจาก setTimeout สามารถดำเนินการเรียกกลับได้หลังจากล่าช้าเกินกว่าที่กำหนด มีสาเหตุหลายประการที่เป็นไปได้ และทุกการวนซ้ำจะใช้เวลาในการตั้งเวลาใหม่และรันโค้ดบางส่วนก่อนตั้งค่าการหมดเวลาใหม่ คุณไม่มีทางที่จะแม่นยำกับสิ่งนี้คุณควรพิจารณาผลลัพธ์ที่ช้ากว่าที่คาดไว้เสมอ แต่ตราบใดที่คุณไม่รู้ว่าจะช้าลงเท่าใดการพยายามลดความล่าช้าก็จะไม่แม่นยำเช่นกัน JS ในเบราว์เซอร์ไม่ได้หมายถึงความถูกต้อง
pdepmcp

19

ทั้งหมดนี้เป็นแนวคิดที่ดีในทางทฤษฎีจนกว่าคุณจะเจาะลึก ปัญหาคือคุณไม่สามารถเค้น RAF ได้โดยไม่ต้องยกเลิกการซิงโครไนซ์โดยเอาชนะจุดประสงค์ที่มีอยู่ ดังนั้นคุณปล่อยให้มันทำงานที่ความเร็วเต็มรูปแบบและอัปเดตข้อมูลของคุณในวงแยกต่างหาก , หรือแม้กระทั่งหัวข้อแยกต่างหาก!

ใช่ฉันพูดแล้ว คุณสามารถทำ JavaScript แบบมัลติเธรดได้ในเบราว์เซอร์!

มีสองวิธีที่ฉันรู้ว่าทำงานได้ดีมากโดยไม่ต้องใช้น้ำเปล่าโดยใช้น้ำผลไม้น้อยมากและสร้างความร้อนน้อยลง การกำหนดเวลาและประสิทธิภาพของเครื่องจักรที่แม่นยำเป็นผลลัพธ์สุทธิ

ขออภัยหากเป็นคำพูดเล็กน้อย แต่นี่ไป ...


วิธีที่ 1: อัปเดตข้อมูลผ่าน setInterval และกราฟิกผ่าน RAF

ใช้ setInterval แยกต่างหากสำหรับการอัปเดตค่าการแปลและการหมุนฟิสิกส์การชนและอื่น ๆ เก็บค่าเหล่านั้นไว้ในวัตถุสำหรับองค์ประกอบที่เคลื่อนไหวแต่ละชิ้น กำหนดสตริงการแปลงให้กับตัวแปรในออบเจ็กต์แต่ละ setInterval 'frame' เก็บวัตถุเหล่านี้ไว้ในอาร์เรย์ กำหนดช่วงเวลาของคุณเป็น fps ที่คุณต้องการใน ms: ms = (1000 / fps) สิ่งนี้ช่วยให้นาฬิกาคงที่ซึ่งช่วยให้ fps เท่ากันบนอุปกรณ์ใด ๆ โดยไม่คำนึงถึงความเร็ว RAF อย่ากำหนดการแปลงให้กับองค์ประกอบที่นี่!

ในลูป requestAnimationFrame ให้วนซ้ำผ่านอาร์เรย์ของคุณด้วย old-school for loop - อย่าใช้แบบฟอร์มที่ใหม่กว่าที่นี่มันช้า!

for(var i=0; i<sprite.length-1; i++){  rafUpdate(sprite[i]);  }

ในฟังก์ชัน rafUpdate ของคุณรับสตริงการแปลงจากวัตถุ js ของคุณในอาร์เรย์และ id องค์ประกอบ คุณควรมีองค์ประกอบ "สไปรท์" ของคุณติดอยู่กับตัวแปรแล้วหรือสามารถเข้าถึงได้ง่ายด้วยวิธีการอื่น ๆ เพื่อที่คุณจะได้ไม่เสียเวลา 'รับ' เข้าไปใน RAF การเก็บไว้ในวัตถุที่ตั้งชื่อตามรหัส html ของพวกเขาทำงานได้ดีทีเดียว ตั้งค่าส่วนนั้นก่อนที่จะเข้าสู่ SI หรือ RAF ของคุณ

ใช้ RAF เพื่ออัปเดตการแปลงของคุณเท่านั้นใช้เฉพาะการแปลง 3 มิติ (แม้กระทั่งสำหรับ 2d) และตั้งค่า css "will-change: transform;" กับองค์ประกอบที่จะเปลี่ยนแปลง สิ่งนี้จะทำให้การแปลงของคุณซิงค์กับอัตราการรีเฟรชดั้งเดิมให้มากที่สุดเตะใน GPU และบอกเบราว์เซอร์ว่าจะให้สมาธิอยู่ที่ใดมากที่สุด

ดังนั้นคุณควรมีรหัสเทียมแบบนี้ ...

// refs to elements to be transformed, kept in an array
var element = [
   mario: document.getElementById('mario'),
   luigi: document.getElementById('luigi')
   //...etc.
]

var sprite = [  // read/write this with SI.  read-only from RAF
   mario: { id: mario  ....physics data, id, and updated transform string (from SI) here  },
   luigi: {  id: luigi  .....same  }
   //...and so forth
] // also kept in an array (for efficient iteration)

//update one sprite js object
//data manipulation, CPU tasks for each sprite object
//(physics, collisions, and transform-string updates here.)
//pass the object (by reference).
var SIupdate = function(object){
  // get pos/rot and update with movement
  object.pos.x += object.mov.pos.x;  // example, motion along x axis
  // and so on for y and z movement
  // and xyz rotational motion, scripted scaling etc

  // build transform string ie
  object.transform =
   'translate3d('+
     object.pos.x+','+
     object.pos.y+','+
     object.pos.z+
   ') '+

   // assign rotations, order depends on purpose and set-up. 
   'rotationZ('+object.rot.z+') '+
   'rotationY('+object.rot.y+') '+
   'rotationX('+object.rot.x+') '+

   'scale3d('.... if desired
  ;  //...etc.  include 
}


var fps = 30; //desired controlled frame-rate


// CPU TASKS - SI psuedo-frame data manipulation
setInterval(function(){
  // update each objects data
  for(var i=0; i<sprite.length-1; i++){  SIupdate(sprite[i]);  }
},1000/fps); //  note ms = 1000/fps


// GPU TASKS - RAF callback, real frame graphics updates only
var rAf = function(){
  // update each objects graphics
  for(var i=0; i<sprite.length-1; i++){  rAF.update(sprite[i])  }
  window.requestAnimationFrame(rAF); // loop
}

// assign new transform to sprite's element, only if it's transform has changed.
rAF.update = function(object){     
  if(object.old_transform !== object.transform){
    element[object.id].style.transform = transform;
    object.old_transform = object.transform;
  }
} 

window.requestAnimationFrame(rAF); // begin RAF

สิ่งนี้ช่วยให้การอัปเดตของคุณไปยังออบเจ็กต์ข้อมูลและการแปลงสตริงที่ซิงค์กับอัตรา 'เฟรม' ที่ต้องการใน SI และการกำหนดการแปลงจริงใน RAF ที่ซิงค์กับอัตราการรีเฟรช GPU ดังนั้นการอัปเดตกราฟิกจริงจะอยู่ใน RAF เท่านั้น แต่การเปลี่ยนแปลงข้อมูลและการสร้างสตริงการแปลงจะอยู่ใน SI ดังนั้นจึงไม่มี jankies แต่มีการไหลของ 'เวลา' ในอัตราเฟรมที่ต้องการ


ไหล:

[setup js sprite objects and html element object references]

[setup RAF and SI single-object update functions]

[start SI at percieved/ideal frame-rate]
  [iterate through js objects, update data transform string for each]
  [loop back to SI]

[start RAF loop]
  [iterate through js objects, read object's transform string and assign it to it's html element]
  [loop back to RAF]

วิธีที่ 2. ใส่ SI ใน web-worker อันนี้ FAAAST เนียนกริ๊บ!

เหมือนกับวิธีที่ 1 แต่ใส่ SI ใน web-worker มันจะทำงานบนเธรดที่แยกจากกันโดยสิ้นเชิงจากนั้นออกจากหน้าเพื่อจัดการกับ RAF และ UI เท่านั้น ส่งอาร์เรย์สไปรท์ไปมาเป็น 'วัตถุที่ถ่ายโอนได้' นี่คือ buko อย่างรวดเร็ว ไม่ต้องใช้เวลาในการโคลนหรือทำให้เป็นอนุกรม แต่ไม่เหมือนกับการส่งต่อโดยอ้างอิงเนื่องจากการอ้างอิงจากอีกด้านหนึ่งจะถูกทำลายดังนั้นคุณจะต้องให้ทั้งสองฝ่ายส่งผ่านไปยังอีกด้านหนึ่งและอัปเดตเมื่อมีอยู่เท่านั้น เหมือนการส่งโน้ตไปมากับแฟนสมัยมัธยมปลาย

สามารถอ่านและเขียนได้ครั้งละหนึ่งรายการเท่านั้น สิ่งนี้ใช้ได้ตราบเท่าที่พวกเขาตรวจสอบว่าไม่ได้กำหนดไว้หรือไม่เพื่อหลีกเลี่ยงข้อผิดพลาด RAF นั้นเร็วและจะเตะกลับทันทีจากนั้นไปที่เฟรม GPU จำนวนมากเพียงแค่ตรวจสอบว่าถูกส่งกลับหรือยัง SI ในผู้ปฏิบัติงานบนเว็บจะมีอาร์เรย์สไปรต์เกือบตลอดเวลาและจะอัปเดตข้อมูลตำแหน่งการเคลื่อนไหวและฟิสิกส์ตลอดจนการสร้างสตริงการแปลงใหม่จากนั้นส่งกลับไปยัง RAF ในหน้า

นี่เป็นวิธีที่เร็วที่สุดที่ฉันรู้ในการทำให้องค์ประกอบเคลื่อนไหวผ่านสคริปต์ ฟังก์ชั่นทั้งสองจะทำงานเป็นสองโปรแกรมแยกกันบนเธรดสองเธรดที่แยกจากกันโดยใช้ประโยชน์จากซีพียูแบบมัลติคอร์ในลักษณะที่สคริปต์ js ตัวเดียวไม่มี ภาพเคลื่อนไหวจาวาสคริปต์แบบหลายเธรด

และมันจะทำได้อย่างราบรื่นโดยไม่กระตุก แต่ในอัตราเฟรมที่ระบุจริงโดยมีความแตกต่างน้อยมาก


ผลลัพธ์:

ทั้งสองวิธีนี้จะช่วยให้มั่นใจได้ว่าสคริปต์ของคุณจะทำงานด้วยความเร็วเท่ากันบนพีซีโทรศัพท์แท็บเล็ตและอื่น ๆ (แน่นอนว่าอยู่ภายใต้ความสามารถของอุปกรณ์และเบราว์เซอร์)


หมายเหตุด้านข้าง - ในวิธีที่ 1 หากมีกิจกรรมมากเกินไปใน setInterval ของคุณอาจทำให้ RAF ของคุณช้าลงเนื่องจาก async แบบเธรดเดียว คุณสามารถลดการทำลายกิจกรรมนั้นได้มากกว่าบนเฟรม SI ดังนั้น async จะส่งการควบคุมกลับไปยัง RAF ได้เร็วขึ้น โปรดจำไว้ว่า RAF ไปที่อัตราเฟรมสูงสุด แต่จะซิงค์การเปลี่ยนแปลงกราฟิกกับการแสดงผลดังนั้นจึงสามารถข้ามเฟรม RAF ไม่กี่เฟรมได้ตราบใดที่คุณไม่ข้ามมากกว่า SI เฟรมก็จะไม่กระตุก
jdmayfield

วิธีที่ 2 มีประสิทธิภาพมากกว่าเนื่องจากเป็นการทำงานหลายอย่างในสองลูปโดยไม่สลับไปมาผ่าน async แต่คุณยังคงต้องการหลีกเลี่ยงไม่ให้เฟรม SI ของคุณใช้เวลานานกว่าอัตราเฟรมที่คุณต้องการดังนั้นการแยกกิจกรรม SI อาจยังคงเป็น เป็นที่พึงปรารถนาหากมีการจัดการข้อมูลจำนวนมากที่เกิดขึ้นซึ่งจะต้องใช้กรอบ SI มากกว่าหนึ่งเฟรมจึงจะเสร็จสมบูรณ์
jdmayfield

ฉันคิดว่ามันคุ้มค่าที่จะกล่าวถึงในฐานะที่เป็นข้อสังเกตที่น่าสนใจว่าการใช้ลูปแบบจับคู่แบบนี้จะลงทะเบียนใน Chromes DevTools ที่ GPU ทำงานตามอัตราเฟรมที่ระบุในลูป setInterval! ปรากฏเฉพาะเฟรม RAF ที่การเปลี่ยนแปลงกราฟิกเกิดขึ้นจะถูกนับเป็นเฟรมโดยตัววัด FPS ดังนั้นเฟรม RAF ที่มีเฉพาะงานที่ไม่ใช่กราฟิกหรือแม้แต่ลูปเปล่าก็ไม่นับรวมถึง GPU ที่เกี่ยวข้อง ฉันคิดว่าสิ่งนี้น่าสนใจเป็นจุดเริ่มต้นสำหรับการค้นคว้าเพิ่มเติม
jdmayfield

ฉันเชื่อว่าโซลูชันนี้มีปัญหาที่จะทำงานต่อไปเมื่อ rAF ถูกระงับเช่นเนื่องจากผู้ใช้เปลี่ยนไปใช้แท็บอื่น
N4ppeL

1
ป.ล. ฉันอ่านไปบ้างแล้วและดูเหมือนว่าเบราว์เซอร์ส่วนใหญ่จะ จำกัด เหตุการณ์ที่กำหนดเวลาไว้ที่หนึ่งครั้งต่อวินาทีในแท็บพื้นหลังอยู่ดี (ซึ่งควรจะจัดการด้วยวิธีใดวิธีหนึ่ง) หากคุณยังคงต้องการแก้ไขปัญหาและหยุดชั่วคราวโดยสมบูรณ์เมื่อมองไม่เห็นดูเหมือนว่าจะมีvisibilitychangeเหตุการณ์นั้นเกิดขึ้น
N4ppeL

5

วิธีเค้นไปยัง FPS เฉพาะอย่างง่ายดาย:

// timestamps are ms passed since document creation.
// lastTimestamp can be initialized to 0, if main loop is executed immediately
var lastTimestamp = 0,
    maxFPS = 30,
    timestep = 1000 / maxFPS; // ms for each frame

function main(timestamp) {
    window.requestAnimationFrame(main);

    // skip if timestep ms hasn't passed since last frame
    if (timestamp - lastTimestamp < timestep) return;

    lastTimestamp = timestamp;

    // draw frame here
}

window.requestAnimationFrame(main);

ที่มา: คำอธิบายโดยละเอียดเกี่ยวกับลูปเกม JavaScript และเวลาโดย Isaac Sukin


2
หากจอภาพของฉันทำงานที่ 60 FPS และฉันต้องการให้เกมของฉันทำงานที่ 58 FPS ฉันตั้งค่า maxFPS = 58 สิ่งนี้จะทำให้มันทำงานที่ 30 FPS เพราะมันจะข้ามทุกเฟรมที่ 2
Curtis

1
ใช่ฉันลองอันนี้เช่นกัน ฉันเลือกที่จะไม่เค้น RAF เอง - เฉพาะการเปลี่ยนแปลงเท่านั้นที่อัปเดตโดย setTimeout อย่างน้อยใน Chrome สิ่งนี้ทำให้ fps ที่มีประสิทธิภาพทำงานตามจังหวะ setTimeouts ตามการอ่านใน DevTools แน่นอนว่าสามารถอัปเดตเฟรมวิดีโอจริงที่ความเร็วของการ์ดแสดงผลและตรวจสอบอัตราการรีเฟรชได้เท่านั้น แต่วิธีนี้ดูเหมือนจะทำงานกับ jankies น้อยที่สุดดังนั้นการควบคุม fps ที่ "ชัดเจน" ที่สุดจึงเป็นสิ่งที่ฉันต้องการ
jdmayfield

1
เนื่องจากฉันติดตามการเคลื่อนไหวทั้งหมดในออบเจ็กต์ JS แยกจาก RAF สิ่งนี้จะช่วยให้ตรรกะของภาพเคลื่อนไหวการตรวจจับการชนกันหรืออะไรก็ตามที่คุณต้องการทำงานในอัตราที่สอดคล้องกันโดยไม่คำนึงถึง RAF หรือ setTimeout ด้วยการคำนวณเพิ่มเติมเล็กน้อย
jdmayfield

1
คำตอบนี้เป็นอัญมณีในมหาสมุทรทั้งหมดและควรเป็นคำตอบที่ได้รับการยอมรับ
Ivanzinho

2

การข้ามrequestAnimationFrameทำให้ภาพเคลื่อนไหวไม่ราบรื่น (ต้องการ) ที่ fps ที่กำหนดเอง

// Input/output DOM elements
var $results = $("#results");
var $fps = $("#fps");
var $period = $("#period");

// Array of FPS samples for graphing

// Animation state/parameters
var fpsInterval, lastDrawTime, frameCount_timed, frameCount, lastSampleTime, 
		currentFps=0, currentFps_timed=0;
var intervalID, requestID;

// Setup canvas being animated
var canvas = document.getElementById("c");
var canvas_timed = document.getElementById("c2");
canvas_timed.width = canvas.width = 300;
canvas_timed.height = canvas.height = 300;
var ctx = canvas.getContext("2d");
var ctx2 = canvas_timed.getContext("2d");


// Setup input event handlers

$fps.on('click change keyup', function() {
    if (this.value > 0) {
        fpsInterval = 1000 / +this.value;
    }
});

$period.on('click change keyup', function() {
    if (this.value > 0) {
        if (intervalID) {
            clearInterval(intervalID);
        }
        intervalID = setInterval(sampleFps, +this.value);
    }
});


function startAnimating(fps, sampleFreq) {

    ctx.fillStyle = ctx2.fillStyle = "#000";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx2.fillRect(0, 0, canvas.width, canvas.height);
    ctx2.font = ctx.font = "32px sans";
    
    fpsInterval = 1000 / fps;
    lastDrawTime = performance.now();
    lastSampleTime = lastDrawTime;
    frameCount = 0;
    frameCount_timed = 0;
    animate();
    
    intervalID = setInterval(sampleFps, sampleFreq);
		animate_timed()
}

function sampleFps() {
    // sample FPS
    var now = performance.now();
    if (frameCount > 0) {
        currentFps =
            (frameCount / (now - lastSampleTime) * 1000).toFixed(2);
        currentFps_timed =
            (frameCount_timed / (now - lastSampleTime) * 1000).toFixed(2);
        $results.text(currentFps + " | " + currentFps_timed);
        
        frameCount = 0;
        frameCount_timed = 0;
    }
    lastSampleTime = now;
}

function drawNextFrame(now, canvas, ctx, fpsCount) {
    // Just draw an oscillating seconds-hand
    
    var length = Math.min(canvas.width, canvas.height) / 2.1;
    var step = 15000;
    var theta = (now % step) / step * 2 * Math.PI;

    var xCenter = canvas.width / 2;
    var yCenter = canvas.height / 2;
    
    var x = xCenter + length * Math.cos(theta);
    var y = yCenter + length * Math.sin(theta);
    
    ctx.beginPath();
    ctx.moveTo(xCenter, yCenter);
    ctx.lineTo(x, y);
  	ctx.fillStyle = ctx.strokeStyle = 'white';
    ctx.stroke();
    
    var theta2 = theta + 3.14/6;
    
    ctx.beginPath();
    ctx.moveTo(xCenter, yCenter);
    ctx.lineTo(x, y);
    ctx.arc(xCenter, yCenter, length*2, theta, theta2);

    ctx.fillStyle = "rgba(0,0,0,.1)"
    ctx.fill();
    
    ctx.fillStyle = "#000";
    ctx.fillRect(0,0,100,30);
    
    ctx.fillStyle = "#080";
    ctx.fillText(fpsCount,10,30);
}

// redraw second canvas each fpsInterval (1000/fps)
function animate_timed() {
    frameCount_timed++;
    drawNextFrame( performance.now(), canvas_timed, ctx2, currentFps_timed);
    
    setTimeout(animate_timed, fpsInterval);
}

function animate(now) {
    // request another frame
    requestAnimationFrame(animate);
    
    // calc elapsed time since last loop
    var elapsed = now - lastDrawTime;

    // if enough time has elapsed, draw the next frame
    if (elapsed > fpsInterval) {
        // Get ready for next frame by setting lastDrawTime=now, but...
        // Also, adjust for fpsInterval not being multiple of 16.67
        lastDrawTime = now - (elapsed % fpsInterval);

        frameCount++;
    		drawNextFrame(now, canvas, ctx, currentFps);
    }
}
startAnimating(+$fps.val(), +$period.val());
input{
  width:100px;
}
#tvs{
  color:red;
  padding:0px 25px;
}
H3{
  font-weight:400;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3>requestAnimationFrame skipping <span id="tvs">vs.</span> setTimeout() redraw</h3>
<div>
    <input id="fps" type="number" value="33"/> FPS:
    <span id="results"></span>
</div>
<div>
    <input id="period" type="number" value="1000"/> Sample period (fps, ms)
</div>
<canvas id="c"></canvas><canvas id="c2"></canvas>

รหัสต้นฉบับโดย @tavnab


2
var time = 0;
var time_framerate = 1000; //in milliseconds

function animate(timestamp) {
  if(timestamp > time + time_framerate) {
    time = timestamp;    

    //your code
  }

  window.requestAnimationFrame(animate);
}

โปรดเพิ่มประโยคสองสามประโยคเพื่ออธิบายว่าโค้ดของคุณกำลังทำอะไรเพื่อให้คุณได้รับคะแนนโหวตมากขึ้นสำหรับคำตอบของคุณ
Fuzzy Analysis

1

ฉันมักจะทำวิธีง่ายๆนี้โดยไม่ต้องยุ่งกับการประทับเวลา:

var fps, eachNthFrame, frameCount;

fps = 30;

//This variable specifies how many frames should be skipped.
//If it is 1 then no frames are skipped. If it is 2, one frame 
//is skipped so "eachSecondFrame" is renderd.
eachNthFrame = Math.round((1000 / fps) / 16.66);

//This variable is the number of the current frame. It is set to eachNthFrame so that the 
//first frame will be renderd.
frameCount = eachNthFrame;

requestAnimationFrame(frame);

//I think the rest is self-explanatory
fucntion frame() {
  if (frameCount == eachNthFrame) {
    frameCount = 0;
    animate();
  }
  frameCount++;
  requestAnimationFrame(frame);
}

1
สิ่งนี้จะทำงานเร็วเกินไปหากจอภาพของคุณมีความเร็ว 120 fps
Curtis

1

วิธีแก้ไขปัญหานี้ง่ายๆคือกลับจากลูปการเรนเดอร์หากไม่จำเป็นต้องใช้เฟรมในการเรนเดอร์:

const FPS = 60;
let prevTick = 0;    

function render() 
{
    requestAnimationFrame(render);

    // clamp to fixed framerate
    let now = Math.round(FPS * Date.now() / 1000);
    if (now == prevTick) return;
    prevTick = now;

    // otherwise, do your stuff ...
}

สิ่งสำคัญคือต้องทราบว่า requestAnimationFrame ขึ้นอยู่กับผู้ใช้ตรวจสอบอัตราการรีเฟรช (vsync) ดังนั้นการใช้ requestAnimationFrame สำหรับความเร็วของเกมจะทำให้ไม่สามารถเล่นได้บนจอภาพ 200Hz หากคุณไม่ได้ใช้กลไกการจับเวลาแยกต่างหากในการจำลองของคุณ


0

นี่เป็นคำอธิบายที่ดีที่ฉันพบ: CreativeJS.comเพื่อตัดการเรียก setTimeou) ภายในฟังก์ชันที่ส่งไปยัง requestAnimationFrame ข้อกังวลของฉันกับคำขอ "ธรรมดา" คือแอนนิเมชั่นเฟรม "จะเป็นอย่างไรถ้าฉันต้องการให้มันเคลื่อนไหวเพียงสามครั้งต่อวินาที" แม้ว่าจะมี requestAnimationFrame (ซึ่งตรงข้ามกับ setTimeout) ก็ยังคงอยู่สิ้นเปลือง "พลังงาน" (บางส่วน) (หมายความว่ารหัสเบราว์เซอร์กำลังทำบางสิ่งบางอย่างและอาจทำให้ระบบทำงานช้าลง) 60 หรือ 120 หรือกี่ครั้งต่อวินาที ตรงข้ามกับเพียงสองหรือสามครั้งต่อวินาที (ตามที่คุณต้องการ)

ส่วนใหญ่ฉันเรียกใช้เบราว์เซอร์โดยปิด JavaScript อย่างตั้งใจด้วยเหตุผลนี้ แต่ฉันใช้ Yosemite 10.10.3 และฉันคิดว่ามันมีปัญหาในการจับเวลาบางอย่าง - อย่างน้อยก็ในระบบเก่าของฉัน (ค่อนข้างเก่า - หมายถึงปี 2011)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.