ثعبان آخر Android مع Kivy ، Python

مرحبا هناك.

يريد الكثير من الناس بدء تشغيل تطبيقات البرمجة لنظام Android ، لكنهم يفضلون عدم استخدام Android Studio و / أو Java. لماذا؟ لأنه مبالغة. "أريد فقط إنشاء ثعبان ولا شيء أكثر!"



دعونا ثعبان دون جافا! (مع مكافأة في النهاية)

لماذا خلق بعد ثعبان تعليمي آخر؟
إذا كنت مشفرًا بيثون وتريد أن تتعلم gamedev لنظام Android ، فيجب أن تكون قد حصلت بالفعل على "ثعبان على android" ووجدت هذا (Eng) أو ترجمته (Rus) . وكذلك فعلت أنا لسوء الحظ ، وجدت المقالة عديمة الفائدة للغاية بسبب:

كودهم سيء

قضايا بسيطة:

  1. استخدام "تجانب" و "رأس" بدلاً من "تجانب" أو "خلايا" لا معنى له بالنسبة لي. لا يختلف الرأس بشكل كافٍ عن البلاط ليكون متغيرات مختلفة.
  2. يسمى Clock.schedule لـ self.update من ... self.update.
  3. يتم تطبيق ملعب المستوى الثاني في البداية ، بينما يتم تطبيق SnakeApp من المستوى الأول في النهاية.
  4. يتم استخدام أسماء الاتجاهات ("أعلى" ، "لأسفل" ، ...) بدلاً من المتجهات ((0 ، 1) ، (1 ، 0) ...).

القضايا الرئيسية:

  1. يتم إرفاق معظم الكائنات الديناميكية (على سبيل المثال ، الفاكهة) بملف kv ، لذلك لا يمكنك عمل أكثر من تفاحة لأنك يجب أن تعيد كتابة هذا الجزء
  2. منطق غريب لحركة الأفعى بدلاً من حركة "خلية بعد خلية".
  3. رمز منذ فترة طويلة جدا أكثر من 350 خطوط.

المقالة ليست واضحة للمبتدئين

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

  1. الرمز قصير
  2. الثعبان جميل
  3. سيكون للتعليمات تطبيقات واضحة خطوة بخطوة وتقدم سلس من "مرحبًا ، العالم" إلى ثعبان جاهز مع خطوات انتقالية.

النتيجة غير المرغوب فيها


لا توجد مساحة بين الخلايا ، والمثلث محرج ، والرسومات براقة.

تعرف على


التطبيق الأول


يرجى تأكيد أنك قمت بالفعل بتثبيت Kivy (إن لم يكن ، اتبع التعليمات ) وقمت بتشغيل
buildozer init في دليل المشروع.

لنقم بتشغيل أول تطبيق لدينا:

main.py

 from kivy.app import App from kivy.uix.widget import Widget class WormApp(App): def build(self): return Widget() if __name__ == '__main__': WormApp().run() 



أنشأنا القطعة. بشكل مشابه ، يمكننا إنشاء زر أو أي عنصر واجهة مستخدم آخر:

 from kivy.app import App from kivy.uix.widget import Widget from kivy.uix.button import Button class WormApp(App): def build(self): self.but = Button() self.but.pos = (100, 100) self.but.size = (200, 200) self.but.text = "Hello, cruel world" self.form = Widget() self.form.add_widget(self.but) return self.form if __name__ == '__main__': WormApp().run() 



نجاح باهر! تهانينا! لقد قمت بإنشاء زر!

ملفات kk


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

 from kivy.app import App from kivy.uix.widget import Widget from kivy.uix.button import Button class Form(Widget): def __init__(self): super().__init__() self.but1 = Button() self.but1.pos = (100, 100) self.add_widget(self.but1) class WormApp(App): def build(self): self.form = Form() return self.form if __name__ == '__main__': WormApp().run() 

ثم ، نقوم بإنشاء ملف "worm.kv".

worm.kv

 <Form>: but2: but_id Button: id: but_id pos: (200, 200) 

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

 class Form(Widget): def __init__(self): super().__init__() self.but1 = Button() self.but1.pos = (100, 100) self.add_widget(self.but1) # self.but2.text = "OH MY" 



الرسومات


ما نقوم به بعد ذلك هو إنشاء عنصر رسومي. أولاً ، نطبقه في worm.kv:

 <Form>: <Cell>: canvas: Rectangle: size: self.size pos: self.pos 

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

 class Cell(Widget): def __init__(self, x, y, size): super().__init__() self.size = (size, size) # As you can see, we can change self.size which is "size" property of a rectangle self.pos = (x, y) class Form(Widget): def __init__(self): super().__init__() self.cell = Cell(100, 100, 30) self.add_widget(self.cell) 



حسنا ، لقد أنشأنا خلية.

طرق غير ضرورية


دعنا نحاول تحريكه. للقيام بذلك ، يجب علينا إضافة وظيفة Form.update وجدولة ذلك.

 from kivy.app import App from kivy.uix.widget import Widget from kivy.clock import Clock class Cell(Widget): def __init__(self, x, y, size): super().__init__() self.size = (size, size) self.pos = (x, y) class Form(Widget): def __init__(self): super().__init__() self.cell = Cell(100, 100, 30) self.add_widget(self.cell) def start(self): Clock.schedule_interval(self.update, 0.01) def update(self, _): self.cell.pos = (self.cell.pos[0] + 2, self.cell.pos[1] + 3) class WormApp(App): def build(self): self.form = Form() self.form.start() return self.form if __name__ == '__main__': WormApp().run() 

سوف تتحرك الخلية عبر النموذج. كما ترون ، يمكننا جدولة أي وظيفة مع عقارب الساعة.

بعد ذلك ، دعونا نجعل حدث اللمس. إعادة كتابة النموذج:

 class Form(Widget): def __init__(self): super().__init__() self.cells = [] def start(self): Clock.schedule_interval(self.update, 0.01) def update(self, _): for cell in self.cells: cell.pos = (cell.pos[0] + 2, cell.pos[1] + 3) def on_touch_down(self, touch): cell = Cell(touch.x, touch.y, 30) self.add_widget(cell) self.cells.append(cell) 

ينشئ كل touch_down خلية بإحداثيات = (touch.x ، touch.y) وحجم 30. ثم ، نضيفها بمثابة عنصر واجهة مستخدم من النموذج AND إلى صفيفنا الخاصة (من أجل الوصول إليها بسهولة).

الآن يمكنك النقر فوق النموذج الخاص بك وإنشاء خلايا.



أنيق الإعدادات


لأننا نريد الحصول على ثعبان جميل ، يجب أن نميز المواقف الرسومية والمواقع الفعلية للخلايا.

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

ملاحظة: لن نفعل ذلك إذا تعاملنا مع on_draw الكلاسيكية. ولكن هنا ليس لدينا برنامج on_draw.

دعنا نصلح ملف worm.kv:

 <Form>: <Cell>: canvas: Rectangle: size: self.graphical_size pos: self.graphical_pos 

و main.py:

 ... from kivy.properties import * ... class Cell(Widget): graphical_size = ListProperty([1, 1]) graphical_pos = ListProperty([1, 1]) def __init__(self, x, y, size, margin=4): super().__init__() self.actual_size = (size, size) self.graphical_size = (size - margin, size - margin) self.margin = margin self.actual_pos = (x, y) self.graphical_pos_attach() def graphical_pos_attach(self): self.graphical_pos = (self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2) ... class Form(Widget): def __init__(self): super().__init__() self.cell1 = Cell(100, 100, 30) self.cell2 = Cell(130, 100, 30) self.add_widget(self.cell1) self.add_widget(self.cell2) ... 



ظهر الهامش بحيث يبدو جميلًا على الرغم من أننا أنشأنا الخلية الثانية باستخدام X = 130 بدلاً من 132. لاحقًا ، سنقوم بالحركة السلسة استنادًا إلى المسافة بين actual_pos و graphical_pos.

ترميز الدودة


تطبيق


التهيئة الأولية في main.py

 class Config: DEFAULT_LENGTH = 20 CELL_SIZE = 25 APPLE_SIZE = 35 MARGIN = 4 INTERVAL = 0.2 DEAD_CELL = (1, 0, 0, 1) APPLE_COLOR = (1, 1, 0, 1) 

(ثق بي ، ستحبها!)

ثم ، قم بتعيين التكوين إلى التطبيق:

 class WormApp(App): def __init__(self): super().__init__() self.config = Config() self.form = Form(self.config) def build(self): self.form.start() return self.form 

أعد كتابة الحرف الأول وابدأ:

 class Form(Widget): def __init__(self, config): super().__init__() self.config = config self.worm = None def start(self): self.worm = Worm(self.config) self.add_widget(self.worm) Clock.schedule_interval(self.update, self.config.INTERVAL) 

ثم ، الخلية:

 class Cell(Widget): graphical_size = ListProperty([1, 1]) graphical_pos = ListProperty([1, 1]) def __init__(self, x, y, size, margin=4): super().__init__() self.actual_size = (size, size) self.graphical_size = (size - margin, size - margin) self.margin = margin self.actual_pos = (x, y) self.graphical_pos_attach() def graphical_pos_attach(self): self.graphical_pos = (self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2) def move_to(self, x, y): self.actual_pos = (x, y) self.graphical_pos_attach() def move_by(self, x, y, **kwargs): self.move_to(self.actual_pos[0] + x, self.actual_pos[1] + y, **kwargs) def get_pos(self): return self.actual_pos def step_by(self, direction, **kwargs): self.move_by(self.actual_size[0] * direction[0], self.actual_size[1] * direction[1], **kwargs) 

نأمل أن يكون الأمر أكثر أو أقل وضوحًا.

وأخيرا الدودة:

 class Worm(Widget): def __init__(self, config): super().__init__() self.cells = [] self.config = config self.cell_size = config.CELL_SIZE self.head_init((100, 100)) for i in range(config.DEFAULT_LENGTH): self.lengthen() def destroy(self): for i in range(len(self.cells)): self.remove_widget(self.cells[i]) self.cells = [] def lengthen(self, pos=None, direction=(0, 1)): # If pos is set, we put the cell in pos, otherwise accordingly to the specified direction if pos is None: px = self.cells[-1].get_pos()[0] + direction[0] * self.cell_size py = self.cells[-1].get_pos()[1] + direction[1] * self.cell_size pos = (px, py) self.cells.append(Cell(*pos, self.cell_size, margin=self.config.MARGIN)) self.add_widget(self.cells[-1]) def head_init(self, pos): self.lengthen(pos=pos) 

دعنا نعطي الحياة ل wormie لدينا.



حركة


الآن سوف نجعلها تتحرك.

انها بسيطة:

 class Worm(Widget): ... def move(self, direction): for i in range(len(self.cells) - 1, 0, -1): self.cells[i].move_to(*self.cells[i - 1].get_pos()) self.cells[0].step_by(direction) 

 class Form(Widget): def __init__(self, config): super().__init__() self.config = config self.worm = None self.cur_dir = (0, 0) def start(self): self.worm = Worm(self.config) self.add_widget(self.worm) self.cur_dir = (1, 0) Clock.schedule_interval(self.update, self.config.INTERVAL) def update(self, _): self.worm.move(self.cur_dir) 



إنه حي! إنه حي!

السيطرة


كما يمكنك الحكم على صورة المعاينة ، ستكون عناصر التحكم في الثعبان كما يلي:



 class Form(Widget): ... def on_touch_down(self, touch): ws = touch.x / self.size[0] hs = touch.y / self.size[1] aws = 1 - ws if ws > hs and aws > hs: cur_dir = (0, -1) # Down elif ws > hs >= aws: cur_dir = (1, 0) # Right elif ws <= hs < aws: cur_dir = (-1, 0) # Left else: cur_dir = (0, 1) # Up self.cur_dir = cur_dir 



بارد.

خلق الفاكهة


أولا ، نحن التهيئة.

 class Form(Widget): ... def __init__(self, config): super().__init__() self.config = config self.worm = None self.cur_dir = (0, 0) self.fruit = None ... def random_cell_location(self, offset): x_row = self.size[0] // self.config.CELL_SIZE x_col = self.size[1] // self.config.CELL_SIZE return random.randint(offset, x_row - offset), random.randint(offset, x_col - offset) def random_location(self, offset): x_row, x_col = self.random_cell_location(offset) return self.config.CELL_SIZE * x_row, self.config.CELL_SIZE * x_col def fruit_dislocate(self): x, y = self.random_location(2) self.fruit.move_to(x, y) ... def start(self): self.fruit = Cell(0, 0, self.config.APPLE_SIZE, self.config.MARGIN) self.worm = Worm(self.config) self.fruit_dislocate() self.add_widget(self.worm) self.add_widget(self.fruit) self.cur_dir = (1, 0) Clock.schedule_interval(self.update, self.config.INTERVAL) 

النتيجة الحالية:



الآن يجب علينا تنفيذ بعض طرق الدودة:

 class Worm(Widget): ... # Here we get all the positions of our cells def gather_positions(self): return [cell.get_pos() for cell in self.cells] # Just check if our head has the same position as another Cell def head_intersect(self, cell): return self.cells[0].get_pos() == cell.get_pos() 

الفوائد الأخرى للتجميع
بالمناسبة ، بعد تنفيذ التجميع ، يمكننا تغيير الفاكهة:

 class Form(Widget): def fruit_dislocate(self): x, y = self.random_location(2) while (x, y) in self.worm.gather_positions(): x, y = self.random_location(2) self.fruit.move_to(x, y) 

عند هذه النقطة ، لن تكون الثمرة موجودة في بلاط الدودة.

... وأضف هذا الاختيار لتحديث ()

 class Form(Widget): ... def update(self, _): self.worm.move(self.cur_dir) if self.worm.head_intersect(self.fruit): directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] self.worm.lengthen(direction=random.choice(directions)) self.fruit_dislocate() 

الكشف عن ضرب البلاط الذاتي


نريد أن نعرف ما إذا كان الرأس لديه نفس موضع إحدى خلايا الدودة.

 class Form(Widget): ... def __init__(self, config): super().__init__() self.config = config self.worm = None self.cur_dir = (0, 0) self.fruit = None self.game_on = True def update(self, _): if not self.game_on: return self.worm.move(self.cur_dir) if self.worm.head_intersect(self.fruit): directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] self.worm.lengthen(direction=random.choice(directions)) self.fruit_dislocate() if self.worm_bite_self(): self.game_on = False def worm_bite_self(self): for cell in self.worm.cells[1:]: if self.worm.head_intersect(cell): return cell return False 



التلوين ، الديكور ، وإعادة بناء الكود


دعنا نبدأ بإعادة بيع الكود.

أعد الكتابة وأضف

 class Form(Widget): ... def start(self): self.worm = Worm(self.config) self.add_widget(self.worm) if self.fruit is not None: self.remove_widget(self.fruit) self.fruit = Cell(0, 0, self.config.APPLE_SIZE) self.fruit_dislocate() self.add_widget(self.fruit) Clock.schedule_interval(self.update, self.config.INTERVAL) self.game_on = True self.cur_dir = (0, -1) def stop(self): self.game_on = False Clock.unschedule(self.update) def game_over(self): self.stop() ... def on_touch_down(self, touch): if not self.game_on: self.worm.destroy() self.start() return ... 

الآن إذا كانت الدودة ميتة (مجمدة) ، إذا نقرت مرة أخرى ، فسيتم إعادة ضبط اللعبة.

الآن دعنا نذهب إلى الديكور والتلوين.

worm.kv

 <Form>: popup_label: popup_label score_label: score_label canvas: Color: rgba: (.5, .5, .5, 1.0) Line: width: 1.5 points: (0, 0), self.size Line: width: 1.5 points: (self.size[0], 0), (0, self.size[1]) Label: id: score_label text: "Score: " + str(self.parent.worm_len) width: self.width Label: id: popup_label width: self.width <Worm>: <Cell>: canvas: Color: rgba: self.color Rectangle: size: self.graphical_size pos: self.graphical_pos 

أعد كتابة WormApp:

 class WormApp(App): def build(self): self.config = Config() self.form = Form(self.config) return self.form def on_start(self): self.form.start() 



دعونا تلوينها. أعد كتابة الخلية بـ .kv:

 <Cell>: canvas: Color: rgba: self.color Rectangle: size: self.graphical_size pos: self.graphical_pos 

أضف هذا إلى الخلية .__ init__

 self.color = (0.2, 1.0, 0.2, 1.0) # 

وهذا إلى Form.start

 self.fruit.color = (1.0, 0.2, 0.2, 1.0) 

عظيم ، استمتع ثعبان الخاص بك



أخيرًا ، سنصنع علامة "انتهت اللعبة"

 class Form(Widget): ... def __init__(self, config): ... self.popup_label.text = "" ... def stop(self, text=""): self.game_on = False self.popup_label.text = text Clock.unschedule(self.update) def game_over(self): self.stop("GAME OVER" + " " * 5 + "\ntap to reset") 

وجعل الخلية ضرب الأحمر:

بدلا من

  def update(self, _): ... if self.worm_bite_self(): self.game_over() ... 

الكتابة

  def update(self, _): cell = self.worm_bite_self() if cell: cell.color = (1.0, 0.2, 0.2, 1.0) self.game_over() 



هل ما زلت الاهتمام؟ القادمة هي الجزء الأكثر إثارة للاهتمام.

قسم المكافآت - حركة سلسة


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

smooth.py

 from kivy.clock import Clock import time class Timing: @staticmethod def linear(x): return x class Smooth: def __init__(self, interval=1.0/60.0): self.objs = [] self.running = False self.interval = interval def run(self): if self.running: return self.running = True Clock.schedule_interval(self.update, self.interval) def stop(self): if not self.running: return self.running = False Clock.unschedule(self.update) def setattr(self, obj, attr, value): exec("obj." + attr + " = " + str(value)) def getattr(self, obj, attr): return float(eval("obj." + attr)) def update(self, _): cur_time = time.time() for line in self.objs: obj, prop_name_x, prop_name_y, from_x, from_y, to_x, to_y, start_time, period, timing = line time_gone = cur_time - start_time if time_gone >= period: self.setattr(obj, prop_name_x, to_x) self.setattr(obj, prop_name_y, to_y) self.objs.remove(line) else: share = time_gone / period acs = timing(share) self.setattr(obj, prop_name_x, from_x * (1 - acs) + to_x * acs) self.setattr(obj, prop_name_y, from_y * (1 - acs) + to_y * acs) if len(self.objs) == 0: self.stop() def move_to(self, obj, prop_name_x, prop_name_y, to_x, to_y, t, timing=Timing.linear): self.objs.append((obj, prop_name_x, prop_name_y, self.getattr(obj, prop_name_x), self.getattr(obj, prop_name_y), to_x, to_y, time.time(), t, timing)) self.run() class XSmooth(Smooth): def __init__(self, props, timing=Timing.linear, *args, **kwargs): super().__init__(*args, **kwargs) self.props = props self.timing = timing def move_to(self, obj, to_x, to_y, t): super().move_to(obj, *self.props, to_x, to_y, t, timing=self.timing) 

لأولئك الذين يكرهون الكود
هذه الوحدة ليست الحل الأكثر أناقة ©. انه حل سيء واعترف به. إنه الحل الوحيد في العالم.

لذلك يمكنك فقط إنشاء smooth.py ونسخ هذا الرمز ولصقه في الملف.
أخيرًا ، دعنا نجعلها تعمل:

 class Form(Widget): ... def __init__(self, config): ... self.smooth = smooth.XSmooth(["graphical_pos[0]", "graphical_pos[1]"]) 

ثم نستبدل self.worm.move () بـ

 class Form(Widget): ... def update(self, _): ... self.worm.move(self.cur_dir, smooth_motion=(self.smooth, self.config.INTERVAL)) 

وهذه هي الطريقة التي ينبغي أن تبدو طرق الخلية

 class Cell(Widget): ... def graphical_pos_attach(self, smooth_motion=None): to_x, to_y = self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2 if smooth_motion is None: self.graphical_pos = to_x, to_y else: smoother, t = smooth_motion smoother.move_to(self, to_x, to_y, t) def move_to(self, x, y, **kwargs): self.actual_pos = (x, y) self.graphical_pos_attach(**kwargs) def move_by(self, x, y, **kwargs): self.move_to(self.actual_pos[0] + x, self.actual_pos[1] + y, **kwargs) 

هذا كل شيء ، شكرا لك على الاهتمام!

كيف تعمل النتيجة النهائية:


الرمز النهائي
main.py
 from kivy.app import App from kivy.uix.widget import Widget from kivy.clock import Clock from kivy.properties import * import random import smooth class Cell(Widget): graphical_size = ListProperty([1, 1]) graphical_pos = ListProperty([1, 1]) color = ListProperty([1, 1, 1, 1]) def __init__(self, x, y, size, margin=4): super().__init__() self.actual_size = (size, size) self.graphical_size = (size - margin, size - margin) self.margin = margin self.actual_pos = (x, y) self.graphical_pos_attach() self.color = (0.2, 1.0, 0.2, 1.0) def graphical_pos_attach(self, smooth_motion=None): to_x, to_y = self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2 if smooth_motion is None: self.graphical_pos = to_x, to_y else: smoother, t = smooth_motion smoother.move_to(self, to_x, to_y, t) def move_to(self, x, y, **kwargs): self.actual_pos = (x, y) self.graphical_pos_attach(**kwargs) def move_by(self, x, y, **kwargs): self.move_to(self.actual_pos[0] + x, self.actual_pos[1] + y, **kwargs) def get_pos(self): return self.actual_pos def step_by(self, direction, **kwargs): self.move_by(self.actual_size[0] * direction[0], self.actual_size[1] * direction[1], **kwargs) class Worm(Widget): def __init__(self, config): super().__init__() self.cells = [] self.config = config self.cell_size = config.CELL_SIZE self.head_init((100, 100)) for i in range(config.DEFAULT_LENGTH): self.lengthen() def destroy(self): for i in range(len(self.cells)): self.remove_widget(self.cells[i]) self.cells = [] def lengthen(self, pos=None, direction=(0, 1)): if pos is None: px = self.cells[-1].get_pos()[0] + direction[0] * self.cell_size py = self.cells[-1].get_pos()[1] + direction[1] * self.cell_size pos = (px, py) self.cells.append(Cell(*pos, self.cell_size, margin=self.config.MARGIN)) self.add_widget(self.cells[-1]) def head_init(self, pos): self.lengthen(pos=pos) def move(self, direction, **kwargs): for i in range(len(self.cells) - 1, 0, -1): self.cells[i].move_to(*self.cells[i - 1].get_pos(), **kwargs) self.cells[0].step_by(direction, **kwargs) def gather_positions(self): return [cell.get_pos() for cell in self.cells] def head_intersect(self, cell): return self.cells[0].get_pos() == cell.get_pos() class Form(Widget): worm_len = NumericProperty(0) def __init__(self, config): super().__init__() self.config = config self.worm = None self.cur_dir = (0, 0) self.fruit = None self.game_on = True self.smooth = smooth.XSmooth(["graphical_pos[0]", "graphical_pos[1]"]) def random_cell_location(self, offset): x_row = self.size[0] // self.config.CELL_SIZE x_col = self.size[1] // self.config.CELL_SIZE return random.randint(offset, x_row - offset), random.randint(offset, x_col - offset) def random_location(self, offset): x_row, x_col = self.random_cell_location(offset) return self.config.CELL_SIZE * x_row, self.config.CELL_SIZE * x_col def fruit_dislocate(self): x, y = self.random_location(2) while (x, y) in self.worm.gather_positions(): x, y = self.random_location(2) self.fruit.move_to(x, y) def start(self): self.worm = Worm(self.config) self.add_widget(self.worm) if self.fruit is not None: self.remove_widget(self.fruit) self.fruit = Cell(0, 0, self.config.APPLE_SIZE) self.fruit.color = (1.0, 0.2, 0.2, 1.0) self.fruit_dislocate() self.add_widget(self.fruit) self.game_on = True self.cur_dir = (0, -1) Clock.schedule_interval(self.update, self.config.INTERVAL) self.popup_label.text = "" def stop(self, text=""): self.game_on = False self.popup_label.text = text Clock.unschedule(self.update) def game_over(self): self.stop("GAME OVER" + " " * 5 + "\ntap to reset") def align_labels(self): try: self.popup_label.pos = ((self.size[0] - self.popup_label.width) / 2, self.size[1] / 2) self.score_label.pos = ((self.size[0] - self.score_label.width) / 2, self.size[1] - 80) except: print(self.__dict__) assert False def update(self, _): if not self.game_on: return self.worm.move(self.cur_dir, smooth_motion=(self.smooth, self.config.INTERVAL)) if self.worm.head_intersect(self.fruit): directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] self.worm.lengthen(direction=random.choice(directions)) self.fruit_dislocate() cell = self.worm_bite_self() if cell: cell.color = (1.0, 0.2, 0.2, 1.0) self.game_over() self.worm_len = len(self.worm.cells) self.align_labels() def on_touch_down(self, touch): if not self.game_on: self.worm.destroy() self.start() return ws = touch.x / self.size[0] hs = touch.y / self.size[1] aws = 1 - ws if ws > hs and aws > hs: cur_dir = (0, -1) elif ws > hs >= aws: cur_dir = (1, 0) elif ws <= hs < aws: cur_dir = (-1, 0) else: cur_dir = (0, 1) self.cur_dir = cur_dir def worm_bite_self(self): for cell in self.worm.cells[1:]: if self.worm.head_intersect(cell): return cell return False class Config: DEFAULT_LENGTH = 20 CELL_SIZE = 25 APPLE_SIZE = 35 MARGIN = 4 INTERVAL = 0.3 DEAD_CELL = (1, 0, 0, 1) APPLE_COLOR = (1, 1, 0, 1) class WormApp(App): def build(self): self.config = Config() self.form = Form(self.config) return self.form def on_start(self): self.form.start() if __name__ == '__main__': WormApp().run() 



smooth.py
 from kivy.clock import Clock import time class Timing: @staticmethod def linear(x): return x class Smooth: def __init__(self, interval=1.0/60.0): self.objs = [] self.running = False self.interval = interval def run(self): if self.running: return self.running = True Clock.schedule_interval(self.update, self.interval) def stop(self): if not self.running: return self.running = False Clock.unschedule(self.update) def setattr(self, obj, attr, value): exec("obj." + attr + " = " + str(value)) def getattr(self, obj, attr): return float(eval("obj." + attr)) def update(self, _): cur_time = time.time() for line in self.objs: obj, prop_name_x, prop_name_y, from_x, from_y, to_x, to_y, start_time, period, timing = line time_gone = cur_time - start_time if time_gone >= period: self.setattr(obj, prop_name_x, to_x) self.setattr(obj, prop_name_y, to_y) self.objs.remove(line) else: share = time_gone / period acs = timing(share) self.setattr(obj, prop_name_x, from_x * (1 - acs) + to_x * acs) self.setattr(obj, prop_name_y, from_y * (1 - acs) + to_y * acs) if len(self.objs) == 0: self.stop() def move_to(self, obj, prop_name_x, prop_name_y, to_x, to_y, t, timing=Timing.linear): self.objs.append((obj, prop_name_x, prop_name_y, self.getattr(obj, prop_name_x), self.getattr(obj, prop_name_y), to_x, to_y, time.time(), t, timing)) self.run() class XSmooth(Smooth): def __init__(self, props, timing=Timing.linear, *args, **kwargs): super().__init__(*args, **kwargs) self.props = props self.timing = timing def move_to(self, obj, to_x, to_y, t): super().move_to(obj, *self.props, to_x, to_y, t, timing=self.timing) 



worm.kv
 <Form>: popup_label: popup_label score_label: score_label canvas: Color: rgba: (.5, .5, .5, 1.0) Line: width: 1.5 points: (0, 0), self.size Line: width: 1.5 points: (self.size[0], 0), (0, self.size[1]) Label: id: score_label text: "Score: " + str(self.parent.worm_len) width: self.width Label: id: popup_label width: self.width <Worm>: <Cell>: canvas: Color: rgba: self.color Rectangle: size: self.graphical_size pos: self.graphical_pos 




رمز ، تعديلها من قبل @ tshirtman
تلقيت بعض المشكلات في الكود الخاص بي ، على سبيل المثال ، اقترح tshirtman ، أحد مساهمي مشروع Kivy ، ألا أقوم بعمل Cells كأدوات مصغّرة ، ولكن بدلاً من ذلك ، أطلب تعليمة Point. ومع ذلك ، لا أجد هذا الكود أسهل في فهمه من الكود ، على الرغم من أنه أجمل بالتأكيد من حيث واجهة المستخدم وتطوير اللعبة. على أي حال ، الكود:
main.py
 from kivy.app import App from kivy.uix.widget import Widget from kivy.clock import Clock from kivy.properties import * import random import smooth class Cell: def __init__(self, x, y): self.actual_pos = (x, y) def move_to(self, x, y): self.actual_pos = (x, y) def move_by(self, x, y): self.move_to(self.actual_pos[0] + x, self.actual_pos[1] + y) def get_pos(self): return self.actual_pos class Fruit(Cell): def __init__(self, x, y): super().__init__(x, y) class Worm(Widget): margin = NumericProperty(4) graphical_poses = ListProperty() inj_pos = ListProperty([-1000, -1000]) graphical_size = NumericProperty(0) def __init__(self, config, **kwargs): super().__init__(**kwargs) self.cells = [] self.config = config self.cell_size = config.CELL_SIZE self.head_init((self.config.CELL_SIZE * random.randint(3, 5), self.config.CELL_SIZE * random.randint(3, 5))) self.margin = config.MARGIN self.graphical_size = self.cell_size - self.margin for i in range(config.DEFAULT_LENGTH): self.lengthen() def destroy(self): self.cells = [] self.graphical_poses = [] self.inj_pos = [-1000, -1000] def cell_append(self, pos): self.cells.append(Cell(*pos)) self.graphical_poses.extend([0, 0]) self.cell_move_to(len(self.cells) - 1, pos) def lengthen(self, pos=None, direction=(0, 1)): if pos is None: px = self.cells[-1].get_pos()[0] + direction[0] * self.cell_size py = self.cells[-1].get_pos()[1] + direction[1] * self.cell_size pos = (px, py) self.cell_append(pos) def head_init(self, pos): self.lengthen(pos=pos) def cell_move_to(self, i, pos, smooth_motion=None): self.cells[i].move_to(*pos) to_x, to_y = pos[0], pos[1] if smooth_motion is None: self.graphical_poses[i * 2], self.graphical_poses[i * 2 + 1] = to_x, to_y else: smoother, t = smooth_motion smoother.move_to(self, "graphical_poses[" + str(i * 2) + "]", "graphical_poses[" + str(i * 2 + 1) + "]", to_x, to_y, t) def move(self, direction, **kwargs): for i in range(len(self.cells) - 1, 0, -1): self.cell_move_to(i, self.cells[i - 1].get_pos(), **kwargs) self.cell_move_to(0, (self.cells[0].get_pos()[0] + self.cell_size * direction[0], self.cells[0].get_pos()[1] + self.cell_size * direction[1]), **kwargs) def gather_positions(self): return [cell.get_pos() for cell in self.cells] def head_intersect(self, cell): return self.cells[0].get_pos() == cell.get_pos() class Form(Widget): worm_len = NumericProperty(0) fruit_pos = ListProperty([0, 0]) fruit_size = NumericProperty(0) def __init__(self, config, **kwargs): super().__init__(**kwargs) self.config = config self.worm = None self.cur_dir = (0, 0) self.fruit = None self.game_on = True self.smooth = smooth.Smooth() def random_cell_location(self, offset): x_row = self.size[0] // self.config.CELL_SIZE x_col = self.size[1] // self.config.CELL_SIZE return random.randint(offset, x_row - offset), random.randint(offset, x_col - offset) def random_location(self, offset): x_row, x_col = self.random_cell_location(offset) return self.config.CELL_SIZE * x_row, self.config.CELL_SIZE * x_col def fruit_dislocate(self, xy=None): if xy is not None: x, y = xy else: x, y = self.random_location(2) while (x, y) in self.worm.gather_positions(): x, y = self.random_location(2) self.fruit.move_to(x, y) self.fruit_pos = (x, y) def start(self): self.worm = Worm(self.config) self.add_widget(self.worm) self.fruit = Fruit(0, 0) self.fruit_size = self.config.APPLE_SIZE self.fruit_dislocate() self.game_on = True self.cur_dir = (0, -1) Clock.schedule_interval(self.update, self.config.INTERVAL) self.popup_label.text = "" def stop(self, text=""): self.game_on = False self.popup_label.text = text Clock.unschedule(self.update) def game_over(self): self.stop("GAME OVER" + " " * 5 + "\ntap to reset") def align_labels(self): self.popup_label.pos = ((self.size[0] - self.popup_label.width) / 2, self.size[1] / 2) self.score_label.pos = ((self.size[0] - self.score_label.width) / 2, self.size[1] - 80) def update(self, _): if not self.game_on: return self.worm.move(self.cur_dir, smooth_motion=(self.smooth, self.config.INTERVAL)) if self.worm.head_intersect(self.fruit): directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] self.worm.lengthen(direction=random.choice(directions)) self.fruit_dislocate() cell = self.worm_bite_self() if cell is not None: self.worm.inj_pos = cell.get_pos() self.game_over() self.worm_len = len(self.worm.cells) self.align_labels() def on_touch_down(self, touch): if not self.game_on: self.worm.destroy() self.start() return ws = touch.x / self.size[0] hs = touch.y / self.size[1] aws = 1 - ws if ws > hs and aws > hs: cur_dir = (0, -1) elif ws > hs >= aws: cur_dir = (1, 0) elif ws <= hs < aws: cur_dir = (-1, 0) else: cur_dir = (0, 1) self.cur_dir = cur_dir def worm_bite_self(self): for cell in self.worm.cells[1:]: if self.worm.head_intersect(cell): return cell return None class Config: DEFAULT_LENGTH = 20 CELL_SIZE = 26 # DO NOT FORGET THAT CELL_SIZE - MARGIN WILL BE DIVIDED BY 4 APPLE_SIZE = 36 MARGIN = 2 INTERVAL = 0.3 DEAD_CELL = (1, 0, 0, 1) APPLE_COLOR = (1, 1, 0, 1) class WormApp(App): def __init__(self, **kwargs): super().__init__(**kwargs) self.form = None def build(self, **kwargs): self.config = Config() self.form = Form(self.config, **kwargs) return self.form def on_start(self): self.form.start() if __name__ == '__main__': WormApp().run() 


smooth.py
 from kivy.clock import Clock import time class Timing: @staticmethod def linear(x): return x class Smooth: def __init__(self, interval=1.0/60.0): self.objs = [] self.running = False self.interval = interval def run(self): if self.running: return self.running = True Clock.schedule_interval(self.update, self.interval) def stop(self): if not self.running: return self.running = False Clock.unschedule(self.update) def set_attr(self, obj, attr, value): exec("obj." + attr + " = " + str(value)) def get_attr(self, obj, attr): return float(eval("obj." + attr)) def update(self, _): cur_time = time.time() for line in self.objs: obj, prop_name_x, prop_name_y, from_x, from_y, to_x, to_y, start_time, period, timing = line time_gone = cur_time - start_time if time_gone >= period: self.set_attr(obj, prop_name_x, to_x) self.set_attr(obj, prop_name_y, to_y) self.objs.remove(line) else: share = time_gone / period acs = timing(share) self.set_attr(obj, prop_name_x, from_x * (1 - acs) + to_x * acs) self.set_attr(obj, prop_name_y, from_y * (1 - acs) + to_y * acs) if len(self.objs) == 0: self.stop() def move_to(self, obj, prop_name_x, prop_name_y, to_x, to_y, t, timing=Timing.linear): self.objs.append((obj, prop_name_x, prop_name_y, self.get_attr(obj, prop_name_x), self.get_attr(obj, prop_name_y), to_x, to_y, time.time(), t, timing)) self.run() class XSmooth(Smooth): def __init__(self, props, timing=Timing.linear, *args, **kwargs): super().__init__(*args, **kwargs) self.props = props self.timing = timing def move_to(self, obj, to_x, to_y, t): super().move_to(obj, *self.props, to_x, to_y, t, timing=self.timing) 


worm.kv
 <Form>: popup_label: popup_label score_label: score_label canvas: Color: rgba: (.5, .5, .5, 1.0) Line: width: 1.5 points: (0, 0), self.size Line: width: 1.5 points: (self.size[0], 0), (0, self.size[1]) Color: rgba: (1.0, 0.2, 0.2, 1.0) Point: points: self.fruit_pos pointsize: self.fruit_size / 2 Label: id: score_label text: "Score: " + str(self.parent.worm_len) width: self.width Label: id: popup_label width: self.width <Worm>: canvas: Color: rgba: (0.2, 1.0, 0.2, 1.0) Point: points: self.graphical_poses pointsize: self.graphical_size / 2 Color: rgba: (1.0, 0.2, 0.2, 1.0) Point: points: self.inj_pos pointsize: self.graphical_size / 2 


لا تتردد في طرح أي أسئلة.

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


All Articles