[javascript] التحكم في fps مع requestAnimationFrame؟


Answers

التحديث 2016/6

تكمن المشكلة في اختصار معدل الإطارات في أن معدل التحديث الثابت للشاشة يبلغ 60 إطارًا في الثانية عادةً.

إذا كنا نريد 24 إطارًا في الثانية ، فلن نحصل على 24 إطارًا في الثانية صحيحًا على الشاشة ، يمكننا أن نوفر الوقت على هذا النحو ولكن لن نعرضه حيث أن الشاشة يمكنها فقط عرض إطارات متزامنة بمعدل 15 إطارًا في الثانية و 30 إطارًا في الثانية أو 60 إطارًا في الثانية (بعض الشاشات أيضًا 120 إطارًا في الثانية ).

ومع ذلك ، لأغراض التوقيت ، يمكننا حساب وتحديث متى أمكن ذلك.

يمكنك إنشاء كل المنطق للتحكم في معدل الإطار عن طريق تغليف الحسابات وعمليات الاستدعاء في كائن:

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 هرتز).

إذا كنت ترغب في الحصول على FPS أكثر تعسفًا ، فلا يوجد أي نقطة باستخدام سلاح الجو الملكي حيث أن معدل الإطارات لن يتطابق مطلقًا مع تردد تحديث الشاشة على أي حال (فقط إطار هنا وهناك) والذي لا يمكن ببساطة أن يمنحك حركة سلسة (كما هو الحال مع جميع فترات إعادة ضبط الإطارات ) ويمكنك أيضًا استخدام 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 أولاً عندما يتم استخدام poly-fill) هو أن هذا سيكون أكثر دقة حيث أن setTimeout سوف يصطف على الفور حدثًا عندما تبدأ الحلقة بحيث لا يهم كم من الوقت المتبقي سيستخدم الرمز (بشرط ألا يتجاوز فترة المهلة) ستكون المكالمة التالية في الفترة الزمنية التي تمثلها (بالنسبة لسلاح الجو الملكي النقي ، هذا ليس ضروريًا لأن سلاح الجو الملكي سيحاول القفز إلى الإطار التالي في أي حال).

وتجدر الإشارة أيضًا إلى أن وضعه أولاً سيؤدي أيضًا إلى المخاطرة بتكديس المكالمات مع 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 إلخ.

Question

يبدو أن requestAnimationFrame هو الطريقة الفعلية لتحريك الأشياء الآن. عملت بشكل جيد بالنسبة لي بالنسبة للجزء الأكبر ، ولكن في الوقت الحالي أحاول القيام ببعض الرسوم المتحركة قماش وكنت أتساءل: هل هناك أي طريقة للتأكد من أنها تعمل في إطار حماية بعض الملفات؟ أفهم أن الغرض من سلاح الجو الملكي هو الرسوم المتحركة السلسة باستمرار ، وقد أتعرض لخطر تشوش الرسوم المتحركة ، ولكن يبدو الآن أنه يعمل بسرعات مختلفة جذريًا بشكل كبير ، وأتساءل عما إذا كان هناك طريقة لمكافحة هذا بطريقة ما.

سأستخدم setInterval لكني أرغب في التحسينات التي يقدمها سلاح الجو الملكي البريطاني (خاصة التوقف التلقائي عندما تكون علامة التبويب في البؤرة).

في حال أراد أحدهم الاطلاع على شفرتي ، فسيكون ذلك إلى حد كبير:

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 () هو مجرد بعض التعليمات البرمجية التي تحدد دائرة نصف قطرها استنادا إلى متغير مضادة ثم يرسم دائرة.




هذه كلها أفكار جيدة من الناحية النظرية ، حتى تتعمق. المشكلة هي أنه لا يمكنك خنق سلاح الجو الملكي دون تزييفه ، متغلبًا على هدفه الأساسي. لذلك تركت تشغيله بسرعة كاملة ، وتحديث البيانات الخاصة بك في حلقة منفصلة ، أو حتى موضوع منفصل!

نعم ، قلت ذلك. يمكنك عمل JavaScript متعددة في المتصفح!

هناك طريقتان أعرف أنهما يعملان بشكل جيد للغاية بدون استخدام السوائل ، باستخدام عصير أقل بكثير وخلق حرارة أقل. إن التوقيت الدقيق للجسم البشري وكفاءة الماكينة هما النتيجة النهائية.

أعتذر إذا كان هذا قليل الكلام ، لكن هنا ...

الطريقة الأولى: تحديث البيانات عبر setInterval ، والرسومات عبر RAF.

استخدم setInterval منفصل لتحديث قيم الترجمة والتدوير ، والفيزياء ، والتصادمات ، الخ. احتفظ بتلك القيم في كائن لكل عنصر متحرك. تعيين سلسلة التحويل إلى متغير في الكائن كل "الإطار" setInterval. احتفظ بهذه الكائنات في مصفوفة. عيّن الفاصل الزمني الخاص بك إلى fps المطلوب في ms: ms = (1000 / fps). هذا يحافظ على ساعة ثابتة تسمح لنفس الإطار في الثانية على أي جهاز ، بغض النظر عن سرعة RAF. لا تقم بتعيين التحويلات للعناصر هنا!

في حلقة requestAnimationFrame ، قم بالتكبير من خلال الصفيف الخاص بك مع مدرسة قديمة للحلقة - لا تستخدم الأشكال الأحدث هنا ، فهي بطيئة!

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

في الدالة rafUpdate ، احصل على سلسلة التحويل من كائن js في الصفيف ، ومعرف عناصره. يجب أن يكون لديك بالفعل عناصر "sprite" مرفقة بمتغير أو يمكن الوصول إليها بسهولة من خلال وسائل أخرى حتى لا تفقد الوقت "في وضعها في RAF. الاحتفاظ بها في كائن مسمى بعد عمل معرف html الخاص به يعمل بشكل جيد. اضبط هذا الجزء حتى قبل أن يصل إلى SI أو RAF.

استخدم RAF لتحديث تحويلاتك فقط ، استخدم فقط التحويلات ثلاثية الأبعاد (حتى 2d) ، واضبط css "will-change: transform؛" على العناصر التي ستتغير. هذا يحافظ على مزامنة التحويلات الخاصة بك مع معدل التحديث الأصلي قدر الإمكان ، والركلات في GPU ، ويخبر المتصفح أين يركز أكثر.

لذلك يجب أن يكون لديك شيء مثل هذا pseudocode ...

// 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 مع معدل تحديث وحدة معالجة الرسومات. لذا فإن تحديثات الرسومات الفعلية موجودة فقط في 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 في عامل ويب. هذا واحد هو FAAAST وسلس!

نفس الأسلوب 1 ، ولكن ضع SI في عامل ويب. سيتم تشغيله على موضوع منفصل تمامًا ، ثم يترك الصفحة للتعامل فقط مع RAF و UI. قم بتمرير مصفوفة الرموز المتحركة ذهابًا وإيابًا باسم "كائن قابل للنقل". هذا هو buko سريع. لا يستغرق الاستنساخ أو التسلسل وقتًا ، ولكنه لا يشبه المرور بالإشارة إلى أنه يتم إتلاف المرجع من الجانب الآخر ، لذلك ستحتاج إلى تمرير الجانبين إلى الجانب الآخر ، وتحديثها فقط عند التواجد ، والفرز مثل تمرير مذكرة ذهابًا وإيابًا مع صديقتك في المدرسة الثانوية.

واحد فقط يستطيع القراءة والكتابة في وقت واحد. هذا جيد طالما أنهم يتحققون إذا لم يتم تحديده لتجنب خطأ. سلاح الجو الملكي هو FAST وسوف يعيده على الفور ، ثم يذهب من خلال مجموعة من إطارات GPU فقط التحقق مما إذا تم إرسالها مرة أخرى بعد. سيحتوي SI في عامل الويب على مصفوفة الرموز المتحركة في معظم الوقت ، وسيقوم بتحديث البيانات الموضعية والحركة والفيزيائية ، بالإضافة إلى إنشاء سلسلة التحويل الجديدة ، ثم تمريرها مرة أخرى إلى RAF في الصفحة.

هذه هي أسرع طريقة أعرفها لتحريك العناصر عبر النص. سيتم تشغيل الوظيفتين كبرنامجين منفصلين ، على خيوط منفصلة ، والاستفادة من وحدة المعالجة المركزية متعددة النواة بطريقة لا يعمل بها برنامج نصي واحد. رسوم متحركة متعددة جافا سكريبت.

وسوف تفعل ذلك بسلاسة دون هزيلة ، ولكن بمعدل الإطار المحدد الفعلي ، مع اختلاف قليل جدا.

نتيجة:

أي من هاتين الطريقتين سيضمن تشغيل النص البرمجي بنفس السرعة على أي جهاز كمبيوتر أو هاتف أو جهاز لوحي ، إلخ (ضمن إمكانات الجهاز والمتصفح ، بالطبع).




كيف تخنق بسهولة إلى 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 بواسطة إسحاق سوكين




Links