نمذجة لعبة محمولة ، ومن أين تبدأ ، وكيفية القيام بذلك. الجزء 2

بالنسبة لأولئك الذين فاتتهم الجزء الأول - الجزء 1
الجزء التالي - الجزء 3

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

لذلك ، نبدأ في جمع كل شيء في كومة




الصواريخ:

فئة الصواريخ قاعدة
using DG.Tweening; using GlobalEventAggregator; using UnityEngine; namespace PlayerRocket { public class Rocket : PlayerRocketBase { [SerializeField] private float pathorrectionTime = 10; private Vector3 movingUp = new Vector3(0, 1, 0); protected override void StartEventReact(ButtonStartPressed buttonStartPressed) { transform.SetParent(null); rocketState = RocketState.MOVE; transform.DORotate(Vector3.zero, pathorrectionTime); } protected override void Start() { base.Start(); EventAggregator.Invoke(new RegisterUser { playerHelper = this }); if (rocketState == RocketState.WAITFORSTART) return; RocketBehaviour(); } private void FixedUpdate() { RocketBehaviour(); } private void RocketBehaviour() { switch (rocketState) { case RocketState.WAITFORSTART: if (inputController.OnTouch && !inputController.OnDrag) rocketHolder.RotateHolder(inputController.worldMousePos); break; case RocketState.MOVE: rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime)); forceModel.AddModificator(); break; case RocketState.STOP: Debug.Log(" "); rigidbody.velocity = Vector3.zero; rigidbody.drag = 50; rocketState = RocketState.COMPLETESTOP; break; case RocketState.COMPLETESTOP: break; default: rocketState = RocketState.COMPLETESTOP; break; } } } } 


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

  1. انتظر البداية
  2. أن يطير
  3. تتأثر المعدلات
  4. للتوقف

بمعنى ، لدينا سلوك / حالة صاروخ مختلفة ، اعتمادًا على الحالة الحالية ، يجب أن يوفر الصاروخ سلوكًا مختلفًا. في البرمجة ، نواجه باستمرار موقفًا حيث يمكن أن يكون للكائن العديد من السلوكيات المختلفة جذريًا.

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

كيف يعمل:

أولاً نبدأ بالتعداد مع الحالات التي نحتاجها:

  public enum RocketState { WAITFORSTART = 0, MOVE = 1, STOP = 2, COMPLETESTOP = 3, } 

في الصف الأم لدينا حقل -
 protected RocketState rocketState; 

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

التالي:

نحدد السلوك نفسه في التحديث ، وهذا يتوقف على قيمة حقل rocketState

  private void FixedUpdate() { RocketBehaviour(); } private void RocketBehaviour() { switch (rocketState) { case RocketState.WAITFORSTART: if (inputController.OnTouch && !inputController.OnDrag) rocketHolder.RotateHolder(inputController.worldMousePos); break; case RocketState.MOVE: rigidbody.AddRelativeForce(Vector3.up*(config.Speed*Time.deltaTime)); forceModel.AddModificator(); break; case RocketState.STOP: Debug.Log(" "); rigidbody.velocity = Vector3.zero; rigidbody.drag = 50; rocketState = RocketState.COMPLETESTOP; break; case RocketState.COMPLETESTOP: break; default: rocketState = RocketState.COMPLETESTOP; break; } } 

سوف أفهم ما يحدث:

  1. عندما ننتظر ، نقوم ببساطة بتدوير الصاروخ باتجاه مؤشر الماوس ، وبالتالي تحديد المسار الأولي
  2. الحالة الثانية - الطيران ، تسريع الصاروخ في الاتجاه الصحيح ، وتحديث نموذج التعديل لظهور الكائنات التي تؤثر على المسار
  3. الحالة الثالثة هي عندما يصل الفريق إلينا للتوقف ، وهنا نقوم بعمل كل شيء حتى يتوقف الصاروخ ويترجم إلى الحالة - لقد توقفنا تمامًا.
  4. الحالة الأخيرة هي أننا لا نفعل شيئًا.

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

اكتشفنا الصاروخ. والخطوة التالية هي كائن بسيط ولكن مضحك - زر البداية.

زر البدء


الوظائف التالية مطلوبة من قِبل النقر عليها ، وأبلغت أنها نقرت عليها.

بدء زر الفصل
 using UnityEngine; using UnityEngine.EventSystems; public class StartButton : MonoBehaviour, IPointerDownHandler { private bool isTriggered; private void ButtonStartPressed() { if (isTriggered) return; isTriggered = true; GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed()); Debug.Log(""); } public void OnPointerDown(PointerEventData eventData) { ButtonStartPressed(); } } public struct ButtonStartPressed { } 


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

إذا قمنا google ، فسنجد مجموعة من أساليب OnMouse ، من بينها نقرة. قد يبدو هذا خيارًا سهلاً ، لكنه سيء ​​جدًا ، بدءًا من حقيقة أنه يعمل غالبًا (هناك العديد من الفروق الدقيقة لتتبع النقرات) ، "عزيزي" ، ينتهي بحقيقة أنه لا يعطي هذا العدد من الكتل من الكعك الموجودة في UnityEngine.EventSystems.

في النهاية ، أوصي باستخدام UnityEngine.EventSystems والواجهات IPointerDownHandler ، IPointerClickHandler. في أساليبهم ، ندرك رد الفعل على الضغط ، ولكن هناك العديد من الفروق الدقيقة.

  1. يجب أن يكون EventSystem حاضرًا في المشهد ، وهذا هو كائن / فئة / مكون من الوحدة ، يتم إنشاؤه عادةً عند إنشاء اللوحة القماشية للواجهة ، ولكن يمكنك أيضًا إنشاءها بنفسك.
  2. يجب أن يكون Physics RayCaster موجودًا على الكاميرا (هذا ثلاثي الأبعاد ، ولرسومات ثنائية الأبعاد يوجد متسابق منفصل)
  3. يجب أن يكون هناك مصادم في المنشأة

في المشروع ، يبدو كما يلي:



الآن يتتبع الكائن النقرة وتسمى هذه الطريقة:

 public void OnPointerDown(PointerEventData eventData) { ButtonStartPressed(); } private void ButtonStartPressed() { if (isTriggered) return; isTriggered = true; GlobalEventAggregator.EventAggregator.Invoke(new ButtonStartPressed()); Debug.Log(""); } 

ما يجري هنا:

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

بعد ذلك ، نسمي الحدث - يتم الضغط على الزر ، والذي تشترك فيه فئة الصواريخ ، ووضع الصاروخ في حالة حركة.

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

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

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

دوران الصواريخ لتحديد مسار البداية



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

لكن بالترتيب:

  1. نحن بحاجة إلى أن يتحول الصاروخ نسبة إلى الكوكب في اتجاه عربة اليد
  2. نحن بحاجة إلى المشبك زاوية الدوران

بالنسبة إلى الدوران بالنسبة للكوكب - يمكنك الدوران برفق حول المحور وحساب محور الدوران ، أو يمكنك ببساطة إنشاء كائن مع دمية متمركزة داخل الكوكب ، وتحريك الصاروخ هناك ، وتدوير الدمية بهدوء حول المحور Z ، وستحتوي الدمية على فئة تحدد سلوك الكائن. سوف الصاروخ تدور معها. الكائن اتصلت RocketHolder. نحن برزت بها.

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

الطبقة RocketHolder
 using UnityEngine; public class RocketHolder : MonoBehaviour { [SerializeField] private float clampAngle = 45; private void Awake() { GlobalEventAggregator.EventAggregator.AddListener(this, (InjectEvent<RocketHolder> obj) => obj.inject(this)); } private float ClampAngle(float angle, float from, float to) { if (angle < 0f) angle = 360 + angle; if (angle > 180f) return Mathf.Max(angle, 360 + from); return Mathf.Min(angle, to); } private Vector3 ClampRotationVectorZ (Vector3 rotation ) { return new Vector3(rotation.x, rotation.y, ClampAngle(rotation.z, -clampAngle, clampAngle)); } public void RotateHolder(Vector3 targetPosition) { var diff = targetPosition - transform.position; diff.Normalize(); float rot_z = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg; transform.rotation = Quaternion.Euler(0f, 0f, rot_z - 90); transform.eulerAngles = ClampRotationVectorZ(transform.rotation.eulerAngles); } } 


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

InputController


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

  1. هل هناك نقرة وإحداثياتها
  2. هل هناك انتقاد عمودي وكم انتقد
  3. هل أعمل مع واجهة / المعدلات

الطبقة InputController
 using System; using UnityEngine; using UnityEngine.EventSystems; public class InputController : MonoBehaviour { public const float DirectionRange = 10; private Vector3 clickedPosition; [Header("     ")] [SerializeField] private float afterThisDistanceWeGonnaDoSwipe = 0.5f; [Header("  ")] [SerializeField] private float speedOfVerticalScroll = 2; public ReactiveValue<float> ReactiveVerticalScroll { get; private set; } public Vector3 worldMousePos => Camera.main.ScreenToWorldPoint(Input.mousePosition); public bool OnTouch { get; private set; } public bool OnDrag { get; private set; } // Start is called before the first frame update private void Awake() { ReactiveVerticalScroll = new ReactiveValue<float>(); GlobalEventAggregator.EventAggregator.AddListener(this, (ImOnDragEvent obj) => OnDrag = obj.IsDragging); GlobalEventAggregator.EventAggregator.AddListener<InjectEvent<InputController>>(this, InjectReact); } private void InjectReact(InjectEvent<InputController> obj) { obj.inject(this); } private void OnEnable() { GlobalEventAggregator.EventAggregator.Invoke(this); } void Start() { GlobalEventAggregator.EventAggregator.Invoke(this); } private void MouseInput() { if (EventSystem.current.IsPointerOverGameObject() && EventSystem.current.gameObject.layer == 5) return; if (Input.GetKeyDown(KeyCode.Mouse0)) clickedPosition = Input.mousePosition; if (Input.GetKey(KeyCode.Mouse0)) { if (OnDrag) return; VerticalMove(); OnTouch = true; return; } OnTouch = false; ReactiveVerticalScroll.CurrentValue = 0; } private void VerticalMove() { if ( Math.Abs(Input.mousePosition.y-clickedPosition.y) < afterThisDistanceWeGonnaDoSwipe) return; var distance = clickedPosition.y + Input.mousePosition.y * speedOfVerticalScroll; if (Input.mousePosition.y > clickedPosition.y) ReactiveVerticalScroll.CurrentValue = distance; else if (Input.mousePosition.y < clickedPosition.y) ReactiveVerticalScroll.CurrentValue = -distance; else ReactiveVerticalScroll.CurrentValue = 0; } // Update is called once per frame void Update() { MouseInput(); } } } 


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

يبدو مثل هذا:

فئة ReactiveValue
 public class ReactiveValue<T> where T: struct { private T currentState; public Action<T> OnChange; public T CurrentValue { get => currentState; set { if (value.Equals(currentState)) return; else { currentState = value; OnChange?.Invoke(currentState); } } } } 


نحن نشترك في OnChange ، ونتعذب إذا تغيرت القيمة فقط.

فيما يتعلق بالنماذج الأولية والهندسة المعمارية - النصائح متشابهة ، والخصائص والأساليب العامة فقط ، يجب تغيير جميع البيانات محليًا فقط. أي معالجة والحسابات - إضافة ما يصل وفقا لطرق منفصلة. نتيجة لذلك ، يمكنك دائمًا تغيير التطبيق / الحسابات ، ولن يؤثر ذلك على المستخدمين الخارجيين للفئة. هذا كل شيء الآن ، في الجزء الأخير - حول المعدلات والواجهة (إسقاط السحب). وأخطط لوضع المشروع في بوابة حتى أستطيع أن أرى / أشعر. إذا كانت لديك أسئلة حول النماذج الأولية - اسأل ، سأحاول الإجابة بوضوح.

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


All Articles