تخطَّ إلى المحتوى

الاستدعاءات الراجعة

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

عملية تشغيل رسم متحرك بفعالية من البداية إلى النهاية هي:

  1. استخدم الطريقة Meter.setValue(value) داخل دالة update لإدخال بيانات العدّاد الخام في نسخة من صنف Meter الخاص بك.
  2. إذا قرر Meter أنه مستقر، فسيفعّل دالة الاستدعاء الراجع (callback) الممرَّرة إليه كمعامل ثانٍ.
  3. تحتوي دالة الاستدعاء الراجع هذه على منطق شرطي لتقييم حالة Meter. إذا كانت الحالة صحيحة (truthy)، فيجب إضافة تأثيرك إلى مصفوفة التأثيرات (effects.push(new yourEffect())) أو إلى معالج الحالة (steMgr.Push(new yourEffect())).
  4. في كل تشغيل لدالة update، تُقيَّم مصفوفة التأثيرات بالكامل وآخر عنصر في معالج الحالة. وفي هذه المرحلة، تُرسم التأثيرات استنادًا إلى متغيرات الحالة.
  5. بعد الرسم، يساهم كل تأثير في التحقق من عمره الافتراضي. فالتأثيرات الموجودة في مصفوفة التأثيرات تُنقص ببساطة متغير العمر الافتراضي المعيَّن لنسخة صنفها. وعندما يصبح العمر الافتراضي 0 أو أقل، يُزال التأثير بسهولة باستخدام splice أثناء تقييم مصفوفة التأثيرات. أما التأثيرات الموجودة في معالج الحالة فيمكنها إزالة نفسها عند الحاجة، لأن معالج الحالة يعمل كمكدّس (stack)، ويميل المنطق الشرطي لهذه التأثيرات إلى أن يكون أكثر تعقيدًا.

يبحث هذا العدّاد عن شريط صحة نموذجي، وسيُرجع نسبة اللون المطابق كرقم بين 0 و1. نطاق HSL واسع إلى حد ما في هذه الحالة لمراعاة التدرج في تلوين الشريط. وهذا يطرح مشكلة - فعندما يتقلص شريط الصحة، يكشف في الواقع عن خلفية شفافة قد تتضمن عناصر خضراء من بيئة اللعبة تجتاز فحص اللون وتُربك العدّاد.

<head>
<meta meter="health" tags="example" x= ".05" y=".9" width=".189" h="70-140" s="40-100" l="40-100" type="linear"/>
</head>

هنا، أنشأت نسخة من صنف Meter باسم healthM لتفعيل تأثير “تلقّي الضرر”.

<script>
// Meter instance
var healthM = new Meter(25, healthHelper);
function update () => {
healthM.setValue(health);
window.requestAnimationFrame(update);
}
function healthHelper () => {
// "Took Damage" effect up next
}
// . . .
</script>

لقد وضعت شرط استقرار مرتفعًا نسبيًا (25) لمعالجة مشكلة عدم الاستقرار المذكورة أعلاه. لن تُفعَّل دالة الاستدعاء الراجع المرفقة إلا إذا كانت قيمة العدّاد مستقرة لمدة 25 تحديثًا، مما سيستبعد معظم البيانات التي لا تخص شريط الصحة الفعلي، حتى لو كان اللاعب ثابتًا. والعيب الوحيد في ذلك هو التأخير المتزايد للتأثير، والذي سيظل دائمًا العامل المُقيِّد في هذا السيناريو. بالنسبة لشريط الصحة هذا تحديدًا، يمكننا معالجة ذلك بطريقتين على الأقل:

  • عند تلقّي الضربة، يتحول جزء من الشريط الأخضر فورًا إلى الأحمر قبل أن يتقلص إلى المستوى الجديد من الصحة الخضراء. إذا أضفنا عدّادًا healthRed يبحث عن اللون الأحمر في الموضع نفسه الذي يوجد فيه عدّاد الصحة، فيمكننا دمج قيمتيهما لتشغيل أسرع. وينبغي أن يوفر الشرط if ( healthM.decreased && healthRed.increased ) دقة جيدة في هذه الحالة.
  • يمكننا أيضًا دمج قراءات عدة عدّادات خطية بإعدادات الألوان نفسها. لديّ حاليًا عدّاد خطي واحد مُعَدّ في منتصف شريط الصحة. إذا أعددت عدّادين آخرين، أحدهما أعلى العدّاد الأصلي بقليل والآخر أسفله، مع بقائهما داخل شريط الصحة، فيمكن استخدام تقارب قيم العدّادات الثلاثة كفحص استقرار إضافي. ويجب أن تقرأ العدّادات الثلاثة Meter.decreased وأن تكون قيمها ضمن نطاق صغير من بعضها البعض لتشغيل التأثير.

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

let healthPrev = 0;
function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
effects.push(new healthEffect());
healthPrev = healthM.value;
}
}

تتطلب العبارة الشرطية شرطين صحيحين: انخفاضًا مستقرًا في العدّاد، وعدم تطابق بين قيمة العدّاد الحالية وقيمة مسجّلة سابقًا. أضفت هذا الشرط الثاني لتجنب خلل برمجي. فلو تحققنا من الانخفاض فقط، لتكرر تشغيل التأثير. ونظرًا لأن Meter.decreased تُعيَّن بمجرد استقرار العدّاد، فإن قيمتها تبقى كما هي طالما لم يتغير العدّاد. وبهذه الطريقة، لن يُشغَّل التأثير إلا إذا كان هناك انخفاض يختلف عن آخر قيمة مستقرة.

سأنشئ لك تأثيرين: أحدهما يوضع في مصفوفة التأثيرات، والآخر في معالج الحالة.

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

function healthEffect(color, direction, speed){
// The lifetime of the effect should be iterated in the draw function.
this.lifetime = 200;
this.speed = speed;
this.direction = direction;
this.color = color;
this.draw = () => {
// Set fill from parameter
ctx.fillStyle = `hsl(${this.color}, 100%, 50%)`;
// Check direction
if (this.direction == "up"){
ctx.fillRect(0, this.lifetime, width, 30);
} else {
ctx.fillRect(0, height - this.lifetime, width, 30);
}
// As the lifetime decreases by 5 each update, the rectangle moves accordingly.
this.lifetime -= speed;
}
}
function update() {
// Background color
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 320, 200);
// Draw all effects in array and remove them if they are done.
effects.forEach((ele, i) => {
ele.draw();
if (ele.lifetime == 0){
effects.splice(i, 1);
}
})
window.requestAnimationFrame(update);
}
function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
// Make sure to correctly set the color and direction paremeters.
effects.push(new healthEffect(1, "down", 5));
healthPrev = healthM.value;
}
}

إليك كيف يجب أن يبدو أثناء العمل:

بفضل التصميم القابل للتعديل للدالة، يمكننا بسهولة إنشاء تأثير علاج أيضًا! ما عليك سوى إضافة شرط إضافي إلى دالة الاستدعاء الراجع.

function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
// Damage effect
effects.push(new healthEffect(1, "down", 5));
healthPrev = healthM.value;
}
if (healthM.increased && healthM.value != healthPrev){
// Healing effect
effects.push(new healthEffect(120, "up", 5));
healthPrev = healthM.value;
}
}

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

أنا شخصيًا أخصص معالج الحالة للتأثيرات الكبيرة والمعقدة ذات منطق الحالة المحدد جدًا، خاصةً عندما تريد أن تكون للتأثير الأولوية على جميع التأثيرات الأخرى. ضع في اعتبارك أن معالج الحالة يعمل كمكدّس ولا يشغّل إلا التأثير العلوي؛ وكل تأثير حالة مسؤول عن إزالة نفسه عند الانتهاء. يوضح هذا المثال رسمًا متحركًا لردهة البحث عن مباراة (matchmaking lobby) يتكيف بحسب ما إذا كنت تبحث أم لا. كما يتضمن مصفوفة تأثيرات صغيرة خاصة به مع تأثير مساعد. العدّادان المستخدمان هنا هما inLobby وyellowPlayButton، ولا يُفعَّلان إلا في ردهة البحث عن مباراة. ويتغير لون yellowPlayButton بحسب ما إذا كنت تبحث.

function lobbyAnimation(){
// Instance variables
this.start = new Date().getTime();
this.elapsed = 0;
this.radius = 0;
this.searching = 1000;
// Instance effects array
this.effects = [];
// Process is the method that allows a state effect to remove itself
this.Process = function () {
// If the lobby menu and play button disappear, remove the effect
if (inLobby.decreased && yellowPlayButton.decreased) {
stateMgr.Pop();
// Global boolean to prevent duplicate lobby effects in the state handler
lobbyAnim = false;
}
// If the play button is present, edit this.searching based on its color
yellowPlayButton.value == 1 ? this.searching = 1000 : this.searching = 100;
// Call the Draw function attached to the effect
this.Draw();
};
this.Draw = function(){
// Iterate important variables
this.elapsed = new Date().getTime() - this.start;
this.radius = 50 + (Math.sin(this.elapsed/this.searching)*10);
// Fill the background to overwrite other effects
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, 320, 200);
// Draw the main arc
ctx.beginPath();
ctx.strokeStyle = `hsl(120, 100%, 50%)`;
ctx.lineWidth = 20;
ctx.arc(160, 100, this.radius, 0, 2 * Math.PI);
ctx.stroke();
// If the main arc is the right size, push helper effects into the helper array
if(this.radius > 59 && this.elapsed % 2 == 0){
this.effects.push(new lobbyHelper(160, 100, 69));
}
// Play and remove helper effects if necessary
this.effects.forEach((e, i) => {
ctx.lineWidth = 1;
e.draw();
if (e.radius > 360) {
this.effects.splice(i, 1);
}
});
}
}
function lobbyHelper(x, y, radius){
this.x = x;
this.y = y;
this.radius = radius;
this.draw = function(){
// Set the global transparency value. 1 is opaque, 0 is invisible.
ctx.globalAlpha = 1 - this.radius/360;
ctx.beginPath();
ctx.strokeStyle = `hsl(120, 100%, ${50 + (this.radius/7)}%)`;
ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
ctx.stroke();
// Set the global transparency back to 1 so your other effects are opaque
ctx.globalAlpha = 1;
this.radius++;
}
}
// Declare state handler (found in Snippets)
var stateMgr = new StateHandler();
// Global boolean to prevent duplicate lobby animations
var lobbyAnim = false;
function update(){
// Run the Process function to draw the animation
stateMgr.Process();
}
function lobbyAnimationPlay(){
if(inLobby.value == 1 && yellowPlayButton.value == 1 && !lobbyAnim){
stateMgr.Push(new lobbyAnimation());
lobbyAnim = true;
}
};

بدون بحث:

أثناء البحث:

وبهذا نختتم درس الاستدعاءات الراجعة، بالإضافة إلى جولة التطوير الرسمية في SignalRGB. ترقّب الإضافة التالية والأخيرة إلى جولتنا، صيانة Lightscript!