في هذا المنشور ، سأتحدث عن كيفية تنفيذ تسلسلات من الإجراءات وقطاعات في ألعاب الفيديو. هذه المقالة ترجمة لهذه المقالة وقدمت عرضًا تقديميًا حول نفس الموضوع في Lua في موسكو ، لذلك إذا كنت تفضل مشاهدة الفيديو ، يمكنك مشاهدته هنا .
رمز المقالة مكتوب بلغة Lua ، ولكن يمكن كتابته بسهولة بلغات أخرى (باستثناء الطريقة التي تستخدم coroutines ، لأنها ليست بجميع اللغات).
توضح المقالة كيفية إنشاء آلية تسمح لك بكتابة مقاطع من النموذج التالي:
local function cutscene(player, npc) player:goTo(npc) if player:hasCompleted(quest) then npc:say("You did it!") delay(0.5) npc:say("Thank you") else npc:say("Please help me") end end
الدخول
غالبًا ما يتم العثور على تسلسل الحركة في ألعاب الفيديو. على سبيل المثال ، في مشاهد: الشخصية تلتقي بالعدو ، وتقول له شيئًا ، يجيب العدو ، وما إلى ذلك. يمكن العثور على تسلسل الإجراءات في اللعب. ألق نظرة على هذه الصورة المتحركة:

1. يفتح الباب
2. تدخل الشخصية المنزل
3. يغلق الباب
4. تعتيم الشاشة تدريجيا
5. يتغير المستوى
6. تتلاشى الشاشة بشكل مشرق
7. تدخل الشخصية إلى المقهى
يمكن أيضًا استخدام تسلسل الإجراءات لبرمجة سلوك الشخصيات غير القابلة للعب أو لتنفيذ معارك الرئيس التي يقوم فيها الرئيس ببعض الإجراءات واحدًا تلو الآخر.
المشكلة
إن هيكل حلقة اللعبة القياسية يجعل تنفيذ تسلسل الحركة أمرًا صعبًا. لنفترض أن لدينا حلقة اللعبة التالية:

while game:isRunning() do processInput() dt = clock.delta() update(dt) render() end
نريد تنفيذ المقطع التالي: يقترب اللاعب من المجلس الوطني لنواب الشعب ، ويقول المجلس الوطني لنواب الشعب: "لقد فعلت ذلك!" ، وبعد فترة وجيزة تقول: "شكرًا لك!". في عالم مثالي ، نكتبها على النحو التالي:
player:goTo(npc) npc:say("You did it!") delay(0.5) npc:say("Thank you")
وهنا نواجه مشكلة. يستغرق الأمر بعض الوقت لإكمال الخطوات. قد تنتظر بعض الإجراءات الإدخال من المشغل (على سبيل المثال ، لإغلاق مربع الحوار). بدلاً من وظيفة
delay
، لا يمكنك استدعاء نفس
sleep
- ستبدو اللعبة مجمدة.
دعونا نلقي نظرة على بعض الطرق لحل المشكلة.
بول ، تعداد ، آلات الدولة
إن الطريقة الأكثر وضوحًا لتنفيذ سلسلة من الإجراءات هي تخزين المعلومات حول الحالة الحالية في الأغصان أو الخطوط أو التعدادات. سيبدو الرمز على النحو التالي:
function update(dt) if cutsceneState == 'playerGoingToNpc' then player:continueGoingTo(npc) if player:closeTo(npc) then cutsceneState = 'npcSayingYouDidIt' dialogueWindow:show("You did it!") end elseif cutsceneState == 'npcSayingYouDidIt' then if dialogueWindow:wasClosed() then cutsceneState = 'delay' end elseif ... ...
يؤدي هذا النهج بسهولة إلى رمز السباغيتي وسلاسل طويلة من تعبيرات if-else ، لذلك أوصي بتجنب هذه الطريقة في حل المشكلة.
قائمة الإجراءات
قوائم الإجراءات تشبه إلى حد كبير أجهزة الحالة. قائمة الإجراءات هي قائمة الإجراءات التي يتم تنفيذها واحدة تلو الأخرى. في حلقة اللعبة ، يتم استدعاء وظيفة
update
للإجراء الحالي ، مما يسمح لنا بمعالجة الإدخال وتقديم اللعبة ، حتى إذا استغرق الإجراء وقتًا طويلاً. بعد اكتمال الإجراء ، ننتقل إلى التالي.
في المشهد الذي نريد تنفيذه ، نحتاج إلى تنفيذ الإجراءات التالية: GoToAction و DialogueAction و DelayAction.
لمزيد من الأمثلة ،
سأستخدم مكتبة
الطبقة المتوسطة لـ OOP في Lua.
إليك كيفية تنفيذ
DelayAction
:
قائمة الإجراءات
ActionList:update
تبدو وظيفة
ActionList:update
كما يلي:
function ActionList:update(dt) if not self.isFinished then self.currentAction:update(dt) if self.currentAction.isFinished then self:goToNextAction() if not self.currentAction then self.isFinished = true end end end end
وأخيرًا ، تنفيذ المقطع نفسه:
function makeCutsceneActionList(player, npc) return ActionList:new { GoToAction:new { entity = player, target = npc }, SayAction:new { entity = npc, text = "You did it!" }, DelayAction:new { delay = 0.5 }, SayAction:new { entity = npc, text = "Thank you" } } end
ملاحظة : في لوا ، يمكن إجراء مكالمة إلى
someFunction({ ... })
النحو التالي:
someFunction{...}
. يسمح لك هذا بكتابة
DelayAction:new{ delay = 0.5 }
بدلاً من
DelayAction:new({delay = 0.5})
.
يبدو أفضل بكثير. يظهر الرمز بوضوح تسلسل الإجراءات. إذا أردنا إضافة إجراء جديد ، فيمكننا القيام بذلك بسهولة. من السهل جدًا إنشاء فصول مثل
DelayAction
لجعل
DelayAction
الكتابة أكثر ملاءمة.
أنصحك بمشاهدة عرض Sean Middleditch حول قوائم الإجراءات ، والذي يقدم أمثلة أكثر تعقيدًا.
قوائم الإجراءات مفيدة جدًا بشكل عام. لقد استخدمتها لألعابي لبعض الوقت وكانت بشكل عام سعيدة. لكن هذا النهج له أيضًا عيوب. لنفترض أننا نريد تطبيق مشهد أكثر تعقيدًا قليلاً:
local function cutscene(player, npc) player:goTo(npc) if player:hasCompleted(quest) then npc:say("You did it!") delay(0.5) npc:say("Thank you") else npc:say("Please help me") end end
للقيام بمحاكاة if / else ، تحتاج إلى تنفيذ قوائم غير خطية. يمكن القيام بذلك باستخدام العلامات. يمكن وضع علامة على بعض الإجراءات ، وبعد ذلك ، حسب بعض الشروط ، بدلاً من الانتقال إلى الإجراء التالي ، يمكنك الانتقال إلى إجراء يحتوي على العلامة المطلوبة. يعمل ، ولكن ليس من السهل قراءة وكتابة الوظيفة أعلاه.
تجعل لورا Coroutines هذا الرمز حقيقة.
Coroutines
أساسيات Corua في لوا
Corutin هي وظيفة يمكن إيقافها مؤقتًا ثم استئنافها لاحقًا. يتم تنفيذ Coroutines في نفس الموضوع مثل البرنامج الرئيسي. لم يتم إنشاء أي سلاسل جديدة للكوروتين.
لإيقاف
coroutine.yield
مؤقتًا ، تحتاج إلى استدعاء
coroutine.yield
، لاستئناف -
coroutine.resume
. مثال بسيط:
local function f() print("hello") coroutine.yield() print("world!") end local c = coroutine.create(f) coroutine.resume(c) print("uhh...") coroutine.resume(c)
مخرجات البرنامج:
مرحبا
اه ...
العالم
إليك كيف يعمل. أولاً ، نقوم بإنشاء
coroutine.create
باستخدام
coroutine.create
. بعد هذه المكالمة ، لا يبدأ كوروتين. ولكي يحدث ذلك ، نحتاج إلى تشغيله باستخدام
coroutine.resume
. ثم تسمى الدالة
f
، التي تكتب "مرحبًا"
coroutine.yield
مؤقتًا باستخدام
coroutine.yield
. هذا مشابه
return
، ولكن يمكننا استئناف
f
مع
coroutine.resume
.
إذا قمت بتمرير الحجج عند استدعاء
coroutine.yield
، فستصبح القيم المرجعة للمكالمة المقابلة لـ
coroutine.resume
في "التدفق الرئيسي".
على سبيل المثال:
local function f() ... coroutine.yield(42, "some text") ... end ok, num, text = coroutine.resume(c) print(num, text)
ok
هو متغير يسمح لنا بمعرفة حالة كوروتين. إذا كان
ok
true
، فعندما يكون كل شيء على ما يرام مع coroutine ، لم تحدث أخطاء في الداخل. قيم الإرجاع التي تليها (
num
،
text
) هي نفس الوسيطات التي مررناها
yield
.
إذا كان
ok
false
، فقد حدث خطأ في coroutine ، على سبيل المثال ، تم استدعاء وظيفة
error
داخلها. في هذه الحالة ، ستكون قيمة الإرجاع الثانية رسالة خطأ. مثال على coroutine يحدث فيه خطأ:
local function f() print(1 + notDefined) end c = coroutine.create(f) ok, msg = coroutine.resume(c) if not ok then print("Coroutine failed!", msg) end
الخلاصة:
فشل Coroutine! الإدخال: 4: محاولة إجراء عملية حسابية بقيمة صفرية ("notDefined" عامة)
يمكن الحصول على حالة Coroutine عن طريق استدعاء
coroutine.status
. قد يكون Corutin في الحالات التالية:
- "الجري" - يعمل Coroutine حاليًا. تم استدعاء
coroutine.status
من كوروتين نفسه - "مُعلَّق" - تم إيقاف Corutin مؤقتًا أو لم يتم تشغيله أبدًا
- "عادي" - كوروتين نشط ، ولكن لم يتم تنفيذه. أي أن كوروتين أطلق كوروتين آخر داخله
- "ميتة" - تنفيذ coroutine الكامل (أي ، وظيفة داخل coroutine مكتملة)
الآن ، بمساعدة هذه المعرفة ، يمكننا تنفيذ نظام تسلسل من الإجراءات والقصاصات على أساس coroutines.
خلق مشاهد باستخدام كوروتين
إليك ما ستبدو عليه فئة
Action
الأساسية في النظام الجديد:
function Action:launch() self:init() while not self.finished do local dt = coroutine.yield() self:update(dt) end self:exit() end
يشبه النهج قوائم الإجراءات: يتم استدعاء وظيفة
update
للإجراء حتى اكتمال الإجراء. ولكن هنا نستخدم coroutines ونحقق في كل تكرار لحلقة اللعبة (
Action:launch
يتم استدعاؤه من بعض coroutine). في مكان ما من
update
حلقة اللعبة ، نستأنف تنفيذ المشهد الحالي على النحو التالي:
coroutine.resume(c, dt)
وأخيرًا ، إنشاء مشهد:
function cutscene(player, npc) player:goTo(npc) npc:say("You did it!") delay(0.5) npc:say("Thank you") end
إليك كيفية تنفيذ وظيفة
delay
:
function delay(time) action = DelayAction:new { delay = time } action:launch() end
إنشاء مثل هذه الأغلفة يحسن بشكل كبير من سهولة قراءة شفرة القطع.
DelayAction
تنفيذ
DelayAction
:
هذا التنفيذ مطابق للتطبيق الذي استخدمناه في قوائم العمل! دعونا نلقي نظرة على وظيفة
Action:launch
مرة أخرى:
function Action:launch() self:init() while not self.finished do local dt = coroutine.yield()
الشيء الرئيسي هنا هو
while
، والتي تستمر حتى اكتمال الإجراء. يبدو شيء مثل هذا:

دعنا الآن نلقي نظرة على وظيفة
goTo
:
function Entity:goTo(target) local action = GoToAction:new { entity = self, target = target } action:launch() end function GoToAction:initialize(params) ... end function GoToAction:update(dt) if not self.entity:closeTo(self.target) then ...
Coroutines تسير على ما يرام مع الأحداث. تطبيق فئة
WaitForEventAction
:
function WaitForEventAction:initialize(params) self.finished = false eventManager:subscribe { listener = self, eventType = params.eventType, callback = WaitForEventAction.onEvent } end function WaitForEventAction:onEvent(event) self.finished = true end
هذه الوظيفة لا تحتاج إلى طريقة
update
. سيتم تنفيذه (على الرغم من أنه لن يفعل أي شيء ...) حتى يتلقى حدثًا من النوع المطلوب. هنا هو التطبيق العملي لهذه الفئة - تنفيذ وظيفة
say
:
function Entity:say(text) DialogueWindow:show(text) local action = WaitForEventAction:new { eventType = 'DialogueWindowClosed' } action:launch() end
بسيطة وسهلة القراءة. عند إغلاق مربع الحوار ، فإنه يرسل حدثًا من النوع "DialogueWindowClosed". ينتهي الإجراء القول ويبدأ الإجراء التالي في التنفيذ.
باستخدام coroutine ، يمكنك بسهولة إنشاء مشاهد غير خطية وأشجار حوار:
local answer = girl:say('do_you_love_lua', { 'YES', 'NO' }) if answer == 'YES' then girl:setMood('happy') girl:say('happy_response') else girl:setMood('angry') girl:say('angry_response') end

في هذا المثال ، تكون الدالة
say
أكثر تعقيدًا قليلاً من تلك التي عرضتها سابقًا. يعيد اختيار اللاعب في الحوار ، ولكن ليس من الصعب تنفيذه. على سبيل المثال ، يمكن استخدام
WaitForEventAction
، والذي سيلتقط حدث PlayerChoiceEvent ثم يعيد اختيار المشغل ، والمعلومات التي سيتم تضمينها في كائن الحدث.
أمثلة أكثر تعقيدًا قليلاً
مع coroutine ، يمكنك بسهولة إنشاء برامج تعليمية ومهام صغيرة. على سبيل المثال:
girl:say("Kill that monster!") waitForEvent('EnemyKilled') girl:setMood('happy') girl:say("You did it! Thank you!")

يمكن أيضًا استخدام Coroutines للذكاء الاصطناعي. على سبيل المثال ، يمكنك إنشاء وظيفة يتحرك بها الوحش عبر بعض المسارات:
function followPath(monster, path) local numberOfPoints = path:getNumberOfPoints() local i = 0

عندما يرى الوحش اللاعب ، يمكننا ببساطة التوقف عن تحقيق corotine وإزالته. لذلك ، فإن الحلقة اللانهائية (
while true
) داخل
followPath
ليست لانهائية حقًا.
مع كوروتين ، يمكنك القيام بأعمال "موازية". لن ينتقل المقطع الصوتي إلى الإجراء التالي حتى يكتمل الإجراءان. على سبيل المثال ، سنقوم بعمل مشهد سينمائي حيث تذهب الفتاة والقط إلى نقطة صديق بسرعات مختلفة. بعد وصولهم ، تقول القطة "مواء".
function cutscene(cat, girl, meetingPoint) local c1 = coroutine.create( function() cat:goTo(meetingPoint) end) local c2 = coroutine.create( function() girl:goTo(meetingPoint) end) c1.resume() c2.resume()
الجزء الأكثر أهمية هنا هو دالة
waitForFinish
، وهي عبارة عن غلاف حول فئة
WaitForFinishAction
، والتي يمكن تنفيذها على النحو التالي:
function WaitForFinishAction:update(dt) if coroutine.status(self.c1) == 'dead' and coroutine.status(self.c2) == 'dead' then self.finished = true else if coroutine.status(self.c1) ~= 'dead' then coroutine.resume(self.c1, dt) end if coroutine.status(self.c2) ~= 'dead' then coroutine.resume(self.c2, dt) end end
يمكنك جعل هذه الفئة أكثر قوة إذا سمحت بمزامنة العدد التاسع من الإجراءات.
يمكنك أيضًا إنشاء فصل سينتظر حتى اكتمال
أحد الكائنات ، بدلاً من الانتظار حتى تكتمل
جميع الأنواع. على سبيل المثال ، يمكن استخدامه في ألعاب السباق المصغرة. داخل الرتبة ، سيكون هناك انتظار حتى يصل أحد الدراجين إلى خط النهاية ثم يقوم ببعض تسلسل الإجراءات.
مزايا وعيوب كوروتين
Coroutines هي آلية مفيدة للغاية. باستخدامها ، يمكنك كتابة لقطات الشاشة ورمز اللعب الذي يسهل قراءته وتعديله. يمكن كتابة Cutscenes من هذا النوع بسهولة من قبل المعتدلين أو الأشخاص الذين ليسوا مبرمجين (على سبيل المثال ، مصممي الألعاب أو المستوى).
ويتم تنفيذ كل هذا في مؤشر ترابط واحد ، لذلك لا توجد مشكلة في المزامنة أو
حالة السباق .
النهج له عيوب. على سبيل المثال ، قد تكون هناك مشاكل في الحفظ. لنفترض أن لعبتك تحتوي على برنامج تعليمي طويل يتم تنفيذه مع coroutine. خلال هذا البرنامج التعليمي لن يتمكن اللاعب من الحفظ بسبب للقيام بذلك ، ستحتاج إلى حفظ الحالة الحالية للكوروتين (والتي تتضمن مجموعها الكامل وقيم المتغيرات في الداخل) ، بحيث يمكنك مواصلة البرنامج التعليمي عند التحميل الإضافي من الحفظ.
(
ملاحظة : باستخدام مكتبة
PlutoLibrary ، يمكن إجراء تسلسل لل
corotines ، لكن المكتبة تعمل فقط مع Lua 5.1)
لا تحدث هذه المشكلة مع cutscenes ، كما عادة في الألعاب في منتصف مشهد غير مسموح به.
يمكن حل مشكلة البرنامج التعليمي الطويل إذا قسمته إلى قطع صغيرة. افترض أن لاعبًا قد انتقل إلى الجزء الأول من البرنامج التعليمي ويجب عليه الانتقال إلى غرفة أخرى لمتابعة البرنامج التعليمي. عند هذه النقطة ، يمكنك إنشاء نقطة تفتيش أو إعطاء اللاعب فرصة الحفظ. في الحفظ ، سنكتب شيئًا مثل "اللاعب أكمل الجزء الأول من البرنامج التعليمي". بعد ذلك ، سوف يمر اللاعب بالجزء الثاني من البرنامج التعليمي ، والذي سنستخدم بالفعل مجموعة أخرى منه. وما إلى ذلك ... عند التحميل ، نبدأ فقط في تنفيذ coroutine المطابق للجزء الذي يجب أن يمر به اللاعب.
الخلاصة
كما ترى ، هناك العديد من الأساليب المختلفة لتنفيذ سلسلة من الإجراءات والقصاصات. يبدو لي أن نهج Coroutine قوي للغاية ويسعدني مشاركته مع المطورين. آمل أن هذا الحل للمشكلة سيجعل حياتك أسهل ويسمح لك بعمل لقطات ملحمية في ألعابك.