تطوير لعبة على Svelte 3

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


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


صورة


0. لفارغ الصبر


مستودع تعليمي
مستودع مع إضافات
عرض


1. التحضير


نحن استنساخ قالب للتنمية


git clone https://github.com/sveltejs/template.git 

تثبيت التبعيات.


 cd template/ npm i 

نبدأ خادم ديف.


 npm run dev 

قالبنا متاح في
http: // localhost: 5000 . يدعم الخادم التحديث السريع ، لذلك ستكون تغييراتنا مرئية في المتصفح عند حفظ التغييرات.


إذا كنت لا ترغب في نشر البيئة محليًا ، فيمكنك استخدام أكواد الترميز وصناديق الرمل stackblitz عبر الإنترنت التي تدعم Svelte.


2. إطار اللعبة


يتكون مجلد src من ملفين main.js و App.svelte .
main.js هو نقطة الدخول إلى طلبنا. أثناء التطوير ، لن نلمسها. هنا يتم تثبيت مكون App.svelte في نص المستند.
App.svelte هو مكون من svelte. يتكون قالب المكون من ثلاثة أجزاء:


 <script> // JS   export let name; </script> <style> /* CSS   */ h1 { color: purple; } </style> <!--   --> <h1>Hello {name}!</h1> 

أنماط المكونات معزولة ، لكن من الممكن تعيين أنماط عمومية باستخدام التوجيه: global (). المزيد عن الأنماط .
إضافة أنماط مشتركة لمكوننا.


src / App.svelte
 <script> export let name; </script> <style> :global(html) { height: 100%; /*     100% */ } :global(body) { height: 100%; /*     100% */ overscroll-behavior: none; /*  pull to refresh*/ user-select: none; /*        */ margin: 0; /*  */ background-color: #efefef; /*    */ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; /*   */ } </style> <h1>Hello {name}!</h1> 

لنقم بإنشاء مجلد src / components الذي سيتم تخزين مكوناتنا فيه
في هذا المجلد ، قم بإنشاء ملفين يحتويان على حقل التحكم وعناصر التحكم.


src / مكونات / GameField.svelte
 <div>GameField</div> 

src / مكونات / Controls.svelte
 <div>Controls</div> 

يتم استيراد المكون عن طريق التوجيه


 import Controls from "./components/Controls.svelte"; 

لعرض مكون ، فقط أدخل علامة المكون في الترميز. معرفة المزيد عن العلامات .


 <Controls /> 

الآن نحن استيراد وعرض مكوناتنا في App.svelte.


src / App.svelte
 <script> //   import Controls from "./components/Controls.svelte"; import GameField from "./components/GameField.svelte"; </script> <style> :global(html) { height: 100%; } :global(body) { height: 100%; overscroll-behavior: none; user-select: none; margin: 0; background-color: #efefef; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; } </style> <!--  . ,     , , ,  react --> <Controls /> <GameField /> 

3. الضوابط


يتكون مكون Controls.svelte من ثلاثة أزرار: حرك اليسار ، تحرك لليمين ، إطلاق نار. سيتم عرض أيقونات الأزرار بواسطة عنصر svg.
قم بإنشاء مجلد src / asssets ، نضيف إليه أيقونات svg الخاصة بنا.


src / الأصول / Bullet.svelte
 <svg height="40px" viewBox="0 0 427 427.08344" width="40px"> <path d="m341.652344 38.511719-37.839844 37.839843 46.960938 46.960938 37.839843-37.839844c8.503907-8.527344 15-18.839844 19.019531-30.191406l19.492188-55.28125-55.28125 19.492188c-11.351562 4.019531-21.664062 10.515624-30.191406 19.019531zm0 0" /> <path d="m258.65625 99.078125 69.390625 69.390625 14.425781-33.65625-50.160156-50.160156zm0 0" /> <path d="m.0429688 352.972656 28.2812502-28.285156 74.113281 74.113281-28.28125 28.28125zm0 0" /> <path d="m38.226562 314.789062 208.167969-208.171874 74.113281 74.113281-208.171874 208.171875zm0 0" /> </svg> 

src / الأصول / LeftArrow.svelte
 <svg width="40px" height="40px" viewBox="0 0 292.359 292.359" transform="translate(-5 0)"> <path d="M222.979,5.424C219.364,1.807,215.08,0,210.132,0c-4.949,0-9.233,1.807-12.848,5.424L69.378,133.331 c-3.615,3.617-5.424,7.898-5.424,12.847c0,4.949,1.809,9.233,5.424,12.847l127.906,127.907c3.614,3.617,7.898,5.428,12.848,5.428 c4.948,0,9.232-1.811,12.847-5.428c3.617-3.614,5.427-7.898,5.427-12.847V18.271C228.405,13.322,226.596,9.042,222.979,5.424z" /> </svg> 

src / الأصول / RightArrow.svelte
 <svg width="40px" height="40px" viewBox="0 0 292.359 292.359" transform="translate(5 0) rotate(180)"> <path d="M222.979,5.424C219.364,1.807,215.08,0,210.132,0c-4.949,0-9.233,1.807-12.848,5.424L69.378,133.331 c-3.615,3.617-5.424,7.898-5.424,12.847c0,4.949,1.809,9.233,5.424,12.847l127.906,127.907c3.614,3.617,7.898,5.428,12.848,5.428 c4.948,0,9.232-1.811,12.847-5.428c3.617-3.614,5.427-7.898,5.427-12.847V18.271C228.405,13.322,226.596,9.042,222.979,5.424z" /> </svg> 

إضافة عنصر الزر src / components / IconButton.svelte .
سوف نقبل معالجات الأحداث من المكون الأصل. حتى نتمكن من الضغط على الزر ، نحتاج إلى معالجين: بداية النقرة ونهاية النقرة. نعلن بدء المتغيرات وإطلاقها ، حيث سنقبل معالجات الأحداث لبداية ونهاية النقرة. نحتاج أيضًا إلى المتغير النشط الذي سيتم عرضه في حالة الضغط على الزر أم لا.


 <script> export let start; export let release; export let active; </script> 

نحن أسلب مكوننا


 <style> .iconButton { /*   flex     */ display: flex; align-items: center; justify-content: center; /*    60px */ width: 60px; height: 60px; /*   */ border: 1px solid black; /*    */ border-radius: 50px; /*     */ outline: none; background: transparent; } .active { /*    ,    */ background-color: #bdbdbd; } </style> 

الزر هو عنصر زر يتم من خلاله عرض المحتوى المنقول من مكون أصل. يشار إلى المكان الذي سيتم فيه تحميل المحتوى المنقول بالعلامة <slot />. تعرف على المزيد حول العنصر <slot /> .


 <button> <slot /> </button> 

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


 <button on:mousedown={start} on:touchstart={start} on:mouseup={release} on:touchend={release} class={`iconButton ${active ? 'active' : ''}`}> <slot /> </button> 

نتيجة لذلك ، سيبدو مكوننا كما يلي:


src / components / IconButton.svelte
 <script> export let start; export let release; export let active; </script> <style> .iconButton { display: flex; align-items: center; justify-content: center; width: 60px; height: 60px; border: 1px solid black; border-radius: 50px; outline: none; background: transparent; } .active { background-color: #bdbdbd; } </style> <button on:mousedown={start} on:touchstart={start} on:mouseup={release} on:touchend={release} class={`iconButton ${active ? 'active' : ''}`}> <slot /> </button> 

نحن الآن نستورد أيقوناتنا وعنصر الزر في src / components / Controls.svelte ونقوم بتكوين التصميم.


src / مكونات / Controls.svelte
 <script> //      import IconButton from "./IconButton.svelte"; import LeftArrow from "../assets/LeftArrow.svelte"; import RightArrow from "../assets/RightArrow.svelte"; import Bullet from "../assets/Bullet.svelte"; </script> <style> /*    ,   */ .controls { position: fixed; bottom: 0; left: 0; width: 100%; } /*          */ .container { display: flex; justify-content: space-between; margin: 1rem; } /*     */ .arrowGroup { display: flex; justify-content: space-between; width: 150px; } </style> <div class="controls"> <div class="container"> <div class="arrowGroup"> <IconButton> <LeftArrow /> </IconButton> <IconButton> <RightArrow /> </IconButton> </div> <IconButton> <Bullet /> </IconButton> </div> </div> 

يجب أن يبدو طلبنا كما يلي:
صورة


4. الملعب


ميدان اللعب هو أحد مكونات svg ، حيث سنضيف عناصر لعبتنا (البندقية ، القذائف ، الخصوم).
تحديث src / components / GameField.svelte code


src / مكونات / GameField.svelte
 <style> /*  ,         */ .container { flex-grow: 1; display: flex; flex-direction: column; justify-content: flex-start; max-height: 100%; } </style> <div class="container"> <!--    viewBox          --> <svg viewBox="0 0 480 800"> </svg> </div> 

قم بإنشاء src / components / Cannon.svelte gun . بصوت عال للمستطيل ، ولكن مع ذلك.


src / المكونات / Cannon.svelte
 <style> /*   ,        */ .cannon { transform-origin: 4px 55px; } </style> <!--      svg .   <g>     --> <g class="cannon" transform={`translate(236, 700)`}> <rect width="8" height="60" fill="#212121" /> </g> 

الآن نحن استيراد بندقية لدينا في الملعب.


src / GameField.svelte
 <script> //    import Cannon from "./Cannon.svelte"; </script> <style> .container { flex-grow: 1; display: flex; flex-direction: column; justify-content: flex-start; max-height: 100%; } </style> <div class="container"> <svg viewBox="0 0 480 800"> <!--    --> <Cannon /> </svg> </div> 

5. دورة اللعبة


لدينا الإطار الأساسي للعبة. والخطوة التالية هي إنشاء حلقة لعبة تعالج منطقنا.
لنقم بإنشاء مخازن حيث سيتم احتواء متغيرات منطقنا. سنحتاج إلى مكون قابل للكتابة من وحدة svelte / store. تعلم المزيد عن المتجر .
يبدو إنشاء مستودع بسيط كما يلي:


 //     import { writable } from "svelte/store"; //      null export const isPlaying = writable(null); 

فلنقم بإنشاء src / stores / folder ، وسيتم تخزين جميع القيم القابلة للتغيير لعبتنا هنا.
قم بإنشاء ملف src / stores / game.js حيث سيتم تخزين المتغيرات المسؤولة عن الحالة العامة للعبة.


src / stores / game.js
 //     import { writable } from "svelte/store"; //        ,    true/false export const isPlaying = writable(false); 

قم بإنشاء ملف src / stores / cannon.js ، حيث سيتم تخزين المتغيرات المسؤولة عن حالة السلاح


src / stores / cannon.js
 //     import { writable } from "svelte/store"; //    ,     . //    'left', 'right', null,    export const direction = writable(null); //     export const angle = writable(0); 

يسمح لك Svelte بإنشاء مستودعات مخصصة تتضمن منطق العمل. يمكنك قراءة المزيد عن هذا في الكتاب المدرسي . لا يمكنني دمج هذا بشكل جميل في مفهوم دورة اللعبة ، لذلك نحن فقط نعلن عن المتغيرات في المستودع. سنقوم بإجراء جميع عمليات التلاعب معهم في قسم src / gameLoop .


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


باستخدام التخزين ، يمكنك إنشاء اشتراك في قيمة. سوف نستخدم هذه الوظيفة في المكونات. في الوقت الحالي ، سوف نستخدم دالة get لقراءة قيمة المتغير. سوف نستخدم طريقة .set () للمتغير لضبط القيمة.
يمكنك تحديث القيمة عن طريق استدعاء الأسلوب .update () ، والذي يأخذ دالة كمدخل ، يتم تمرير القيمة الحالية إلى الوسيطة الأولى. مزيد من التفاصيل في الوثائق . كل شيء آخر هو JS النقي.


src / gameLoop / gameLoop.js
 //     import { isPlaying } from '../stores/game'; //    get     ,  . import { get } from 'svelte/store'; //      function startLoop(steps) { window.requestAnimationFrame(() => { //      steps.forEach(step => { //    -  if (typeof step === 'function') step(); }); //    ,    if (get(isPlaying)) startLoop(steps); }); } //       export const startGame = () => { //  ,      true isPlaying.set(true); //   .     startLoop([]); }; //       export function stopGame() { //  ,      false isPlaying.set(false); } 

الآن وصفنا منطق سلوك سلاحنا.


src / gameLoop / cannon.js
 //    get     ,  . import { get } from 'svelte/store'; //      cannon import { angle, direction } from '../stores/cannon.js'; //      export function rotateCannon() { //     const currentAngle = get(angle); //    ,   ,    switch (get(direction)) { //    ""     -45°, //      0.4 case 'left': if (currentAngle > -45) angle.update(a => a - 0.4); break; //    ""     45°, //      0.4 case 'right': if (currentAngle < 45) angle.update(a => a + 0.4); break; default: break; } } 

الآن قم بإضافة معالج تدوير البندقية إلى حلقة اللعبة.


 import { rotateCannon } from "./cannon"; /* ... */ export const startGame = () => { isPlaying.set(true); startLoop([rotateCannon]); }; 

رمز دورة اللعبة الحالية:


src / gameLoop / gameLoop.js
 import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; import { rotateCannon } from './cannon'; //     function startLoop(steps) { window.requestAnimationFrame(() => { steps.forEach(step => { if (typeof step === 'function') step(); }); if (get(isPlaying)) startLoop(steps); }); } export const startGame = () => { isPlaying.set(true); startLoop([rotateCannon]); //      }; export function stopGame() { isPlaying.set(false); } 

لدينا منطق يمكن أن يدور البندقية. لكننا لم نربطها بعد بكبسة الأزرار. حان الوقت للقيام بذلك. ستتم إضافة معالجات الأحداث انقر فوق src / components / Controls.svelte .


 import { direction } from "../stores/cannon.js"; //       //    const resetDirection = () => direction.set(null); const setDirectionLeft = () => direction.set("left"); const setDirectionRight = () => direction.set("right"); 

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


 <IconButton start={setDirectionLeft} release={resetDirection} active={$direction === 'left'}> <LeftArrow /> </IconButton> <IconButton start={setDirectionRight} release={resetDirection} active={$direction === 'right'}> <RightArrow /> </IconButton> 

استخدمنا التعبير $ لمتغير الاتجاه $ . بناء الجملة هذا يجعل القيمة متفاعلة ، الاشتراك تلقائيًا في التغييرات. مزيد من التفاصيل في الوثائق .


src / مكونات / Controls.svelte
 <script> import IconButton from "./IconButton.svelte"; import LeftArrow from "../assets/LeftArrow.svelte"; import RightArrow from "../assets/RightArrow.svelte"; import Bullet from "../assets/Bullet.svelte"; //     import { direction } from "../stores/cannon.js"; //    const resetDirection = () => direction.set(null); const setDirectionLeft = () => direction.set("left"); const setDirectionRight = () => direction.set("right"); </script> <style> .controls { position: fixed; bottom: 0; left: 0; width: 100%; } .container { display: flex; justify-content: space-between; margin: 1rem; } .arrowGroup { display: flex; justify-content: space-between; width: 150px; } </style> <div class="controls"> <div class="container"> <div class="arrowGroup"> <!--        --> <IconButton start={setDirectionLeft} release={resetDirection} active={$direction === 'left'}> <LeftArrow /> </IconButton> <IconButton start={setDirectionRight} release={resetDirection} active={$direction === 'right'}> <RightArrow /> </IconButton> </div> <IconButton> <Bullet /> </IconButton> </div> </div> 

في الوقت الحالي ، عند الضغط على زرنا ، يحدث اختيار ، ولكن البندقية لا تزال لا تدور. نحتاج إلى استيراد قيمة الزاوية إلى مكون Cannon.svelte وتحديث قواعد تحويل التحويل


src / المكونات / Cannon.svelte
 <script> //      import { angle } from "../stores/cannon.js"; </script> <style> .cannon { transform-origin: 4px 55px; } </style> <!--    rotate(${$angle})--> <g class="cannon" transform={`translate(236, 700) rotate(${$angle})`}> <rect width="8" height="60" fill="#212121" /> </g> 

يبقى تشغيل حلقة لعبتنا في مكون App.svelte .


 import { startGame } from "./gameLoop/gameLoop"; startGame(); 

App.svelte
 <script> import Controls from "./components/Controls.svelte"; import GameField from "./components/GameField.svelte"; //     import { startGame } from "./gameLoop/gameLoop"; //  startGame(); </script> <style> :global(html) { height: 100%; } :global(body) { height: 100%; overscroll-behavior: none; user-select: none; margin: 0; background-color: #efefef; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; } </style> <Controls /> <GameField /> 

الصيحة! بدأ سلاحنا في التحرك.
صورة


6. الطلقات


الآن تعليم سلاحنا لإطلاق النار. نحن بحاجة إلى تخزين القيم:


  • هل تطلق النار من البندقية الآن (يتم الضغط على زر النار) ؛
  • الطابع الزمني للرصاصة الأخيرة ، تحتاج إلى حساب معدل إطلاق النار.
  • مجموعة من القذائف.

أضف هذه المتغيرات إلى مستودع src / stores / cannon.js .


src / stores / cannon.js
 import { writable } from 'svelte/store'; export const direction = writable(null); export const angle = writable(0); //   export const isFiring = writable(false); export const lastFireAt = writable(0); export const bulletList = writable([]); 

تحديث الواردات ومنطق اللعبة في src / gameLoop / cannon.js .


src / gameLoop / cannon.js
 import { get } from 'svelte/store'; //   import { angle, direction, isFiring, lastFireAt, bulletList } from '../stores/cannon.js'; export function rotateCannon() { const currentAngle = get(angle); switch (get(direction)) { case 'left': if (currentAngle > -45) angle.update(a => a - 0.4); break; case 'right': if (currentAngle < 45) angle.update(a => a + 0.4); break; default: break; } } //   export function shoot() { //           800 , //          if (get(isFiring) && Date.now() - get(lastFireAt) > 800) { lastFireAt.set(Date.now()); //         . //  id   Math.random    bulletList.update(bullets => [...bullets, { x: 238, y: 760, angle: get(angle), id: () => Math.random() + Date.now() }]); } } //    export function moveBullet() { //    ,      y  -20, //          . //    , ,     . //      , ? bulletList.update(bullets => bullets.map(bullet => ({ ...bullet, y: bullet.y - 20, x: (780 - bullet.y) * Math.tan((bullet.angle * Math.PI) / 180) + 238, })), ); } //    ,     . export function clearBullets() { bulletList.update(bullets => bullets.filter(bullet => bullet.y > 0)); } //     Id. ,        export function removeBullet(id) { bulletList.update(bullets => bullets.filter(bullet => bullet.id !== id)); } 

الآن نستورد معالجاتنا إلى gameLoop.js ونضيفهم إلى حلقة اللعبة.


 import { rotateCannon, shoot, moveBullet, clearBullets } from "./cannon"; /* ... */ export const startGame = () => { isPlaying.set(true); startLoop([rotateCannon, shoot, moveBullet, clearBullets ]); }; 

src / gameLoop / gameLoop.js
 import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; //        import { rotateCannon, shoot, moveBullet, clearBullets } from "./cannon"; function startLoop(steps) { window.requestAnimationFrame(() => { steps.forEach(step => { if (typeof step === 'function') step(); }); if (get(isPlaying)) startLoop(steps); }); } export const startGame = () => { isPlaying.set(true); //      startLoop([rotateCannon, shoot, moveBullet, clearBullets ]); }; export function stopGame() { isPlaying.set(false); } 

الآن علينا فقط إنشاء معالجة للضغط على زر إطلاق النار وإضافة عرض الأصداف في الملعب.
تحرير src / المكونات / Controls.svelte .


 //  ,       import { direction, isFiring } from "../stores/cannon.js"; //      const startFire = () => isFiring.set(true); const stopFire = () => isFiring.set(false); 

أضف الآن معالجاتنا إلى زر التحكم في الحريق ، كما فعلنا مع أزرار الدوران


 <IconButton start={startFire} release={stopFire} active={$isFiring}> <Bullet /> </IconButton> 

src / مكونات / Controls.svelte
 <script> import IconButton from "./IconButton.svelte"; import LeftArrow from "../assets/LeftArrow.svelte"; import RightArrow from "../assets/RightArrow.svelte"; import Bullet from "../assets/Bullet.svelte"; //  ,       import { direction, isFiring } from "../stores/cannon.js"; const resetDirection = () => direction.set(null); const setDirectionLeft = () => direction.set("left"); const setDirectionRight = () => direction.set("right"); //      const startFire = () => isFiring.set(true); const stopFire = () => isFiring.set(false); </script> <style> .controls { position: fixed; bottom: 0; left: 0; width: 100%; } .container { display: flex; justify-content: space-between; margin: 1rem; } .arrowGroup { display: flex; justify-content: space-between; width: 150px; } </style> <div class="controls"> <div class="container"> <div class="arrowGroup"> <IconButton start={setDirectionLeft} release={resetDirection} active={$direction === 'left'}> <LeftArrow /> </IconButton> <IconButton start={setDirectionRight} release={resetDirection} active={$direction === 'right'}> <RightArrow /> </IconButton> </div> <!--     --> <IconButton start={startFire} release={stopFire} active={$isFiring}> <Bullet /> </IconButton> </div> </div> 

يبقى لعرض الأصداف في الملعب. أولا ، إنشاء مكون قذيفة


src / المكونات / Bullet.svelte
 <script> //   bullet  ,    export let bullet; </script> <!--  -  svg  --> <g transform={`translate(${bullet.x}, ${bullet.y}) rotate(${bullet.angle})`}> <rect width="3" height="5" fill="#212121" /> </g> 

نظرًا لأن الأصداف التي تم تخزينها في صفيف ، فإننا نحتاج إلى مكرر لعرضها. في svelte هناك كل توجيه لمثل هذه الحالات. مزيد من التفاصيل في الوثائق .


 //    bulletList,      bullet. //      id  ,  svelte       ,   . //  key   React {#each $bulletList as bullet (bullet.id)} <Bullet {bullet}/> {/each} 

src/components/GameField.svelte
 <script> import Cannon from "./Cannon.svelte"; //    import Bullet from "./Bullet.svelte"; //      import { bulletList } from "../stores/cannon"; </script> <style> .container { flex-grow: 1; display: flex; flex-direction: column; justify-content: flex-start; max-height: 100%; } </style> <div class="container"> <svg viewBox="0 0 480 800"> <!--       --> {#each $bulletList as bullet (bullet.id)} <Bullet {bullet} /> {/each} <Cannon /> </svg> </div> 

.
صورة


7.


ممتاز. . src/stores/enemy.js .


src/stores/enemy.js
 import { writable } from "svelte/store"; //   export const enemyList = writable([]); //      export const lastEnemyAddedAt = writable(0); 

src/gameLoop/enemy.js


src/gameLoop/enemy.js
 import { get } from 'svelte/store'; //      import { enemyList, lastEnemyAddedAt } from '../stores/enemy.js'; //    export function addEnemy() { //         2500 , //     if (Date.now() - get(lastEnemyAddedAt) > 2500) { //      lastEnemyAddedAt.set(Date.now()); //        1  499 // (   ) enemyList.update(enemies => [ ...enemies, { x: Math.floor(Math.random() * 449) + 1, y: 0, id: () => Math.random() + Date.now(), }, ]); } } //   .       0.5 export function moveEnemy() { enemyList.update(enemyList => enemyList.map(enemy => ({ ...enemy, y: enemy.y + 0.5, })), ); } //      id,     export function removeEnemy(id) { enemyList.update(enemies => enemies.filter(enemy => enemy.id !== id)); } 

.


src/gameLoop/gameLoop.js
 import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; import { rotateCannon, shoot, moveBullet, clearBullets } from './cannon'; //      import { addEnemy, moveEnemy } from './enemy'; function startLoop(steps) { window.requestAnimationFrame(() => { steps.forEach(step => { if (typeof step === 'function') step(); }); if (get(isPlaying)) startLoop(steps); }); } export const startGame = () => { isPlaying.set(true); //      startLoop([rotateCannon, shoot, moveBullet, clearBullets, addEnemy, moveEnemy]); }; export function stopGame() { isPlaying.set(false); } 

src/components/Enemy.js .


src/components/Enemy.js
 <script> //   enemy   ,   export let enemy; </script> //    ,     . <g transform={`translate(${enemy.x}, ${enemy.y})`} > <rect width="30" height="30" fill="#212121" /> </g> 

, Each


src/components/GameField.svelte
 <script> import Cannon from "./Cannon.svelte"; import Bullet from "./Bullet.svelte"; //    import Enemy from "./Enemy.svelte"; import { bulletList } from "../stores/cannon"; //      import { enemyList } from "../stores/enemy"; </script> <style> .container { flex-grow: 1; display: flex; flex-direction: column; justify-content: flex-start; max-height: 100%; } </style> <div class="container"> <svg viewBox="0 0 480 800"> <!--       --> {#each $enemyList as enemy (enemy.id)} <Enemy {enemy} /> {/each} {#each $bulletList as bullet (bullet.id)} <Bullet {bullet} /> {/each} <Cannon /> </svg> </div> 

!
صورة


8.


, .
. src/gameLoop/game.js . MDN


src/gameLoop/game.js
 import { get } from 'svelte/store'; //    import { bulletList } from '../stores/cannon'; //    import { enemyList } from '../stores/enemy'; //     import { removeBullet } from './cannon'; //     import { removeEnemy } from './enemy'; //       . //     ,   svg,   , //        . const enemyWidth = 30; const bulletWidth = 5; const enemyHeight = 30; const bulletHeight = 8; //    export function checkCollision() { get(bulletList).forEach(bullet => { get(enemyList).forEach(enemy => { if ( bullet.x < enemy.x + enemyWidth && bullet.x + bulletWidth > enemy.x && bullet.y < enemy.y + enemyHeight && bullet.y + bulletHeight > enemy.y ) { //   ,         removeBullet(bullet.id); removeEnemy(enemy.id); } }); }); } 

.


src/gameLoop/gameLoop.js
 import { isPlaying } from '../stores/game'; import { get } from 'svelte/store'; import { rotateCannon, shoot, moveBullet, clearBullets } from './cannon'; //    import { checkCollision } from './game'; import { addEnemy, moveEnemy } from './enemy'; function startLoop(steps) { window.requestAnimationFrame(() => { steps.forEach(step => { if (typeof step === 'function') step(); }); if (get(isPlaying)) startLoop(steps); }); } export const startGame = () => { isPlaying.set(true); //      startLoop([rotateCannon, shoot, moveBullet, clearBullets, addEnemy, moveEnemy, checkCollision]); }; export function stopGame() { isPlaying.set(false); } 

, .
صورة


9.


, ToDo :


  • , ;
  • ;
  • ;
  • . svelte ;
  • ;
  • . .

github .


استنتاج


, , React. 60 FPS, Svelte .
Svelte , .

Source: https://habr.com/ru/post/ar452684/


All Articles