Hola
Mucha gente quiere comenzar a programar aplicaciones para Android, pero prefieren no usar Android Studio y / o Java. Por qué Porque es una exageración. "¡Solo quiero crear Snake y nada más!"

¡Serpenteemos sin Java! (con un bono al final)
¿Por qué crear otro tutorial de serpiente?Si eres un programador de Python y quieres aprender gamedev para Android, ya debes haber buscado en Google "serpiente en Android" y has encontrado
esto (Eng) o su
traducción (Rus) . Yo también. Desafortunadamente, el artículo me pareció extremadamente inútil porque:
Su código es malo
Problemas menores:
- Usar "mosaico" y "cabeza" en lugar de "mosaicos" o "celdas" no tiene sentido para mí. La cabeza no es lo suficientemente distinta del mosaico como para ser variables diferentes.
- Clock.schedule for self.update se llama desde ... self.update.
- La clase de segundo nivel Playground se implementa al principio, mientras que la clase de primer nivel SnakeApp se implementa al final.
- Los nombres de las direcciones ("arriba", "abajo", ...) se utilizan en lugar de vectores ((0, 1), (1, 0) ...).
Problemas mayores:
- La mayoría de los objetos dinámicos (por ejemplo, la fruta) se adjuntan al archivo kv, por lo que no puede hacer más de 1 manzana porque debería reescribir esta parte
- Lógica extraña para el movimiento de la serpiente en lugar del movimiento "celda tras celda".
- El código es muy largo siendo más de 350 líneas.
El artículo no está claro para los principiantes.
Esta es mi opinión personal. Además, no garantizo que mi tutorial sea más interesante y claro. Pero haré lo mejor que pueda y, para mi artículo, garantizo que:
- El código es corto
- La serpiente es linda
- Las instrucciones tendrán implementaciones claras paso a paso y una progresión suave de "hola, mundo" a una serpiente lista con pasos de transición.
El resultado indeseable

No hay espacio entre las celdas, el triángulo es vergonzoso, los gráficos tienen fallas.
Familiarizarse
Primera aplicación
Por favor, confirme que ya ha instalado Kivy (si no, siga las
instrucciones ) y ejecutó
buildozer init
en el directorio del proyecto.
Ejecutemos nuestra primera aplicación:
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()

Creamos un widget. Análogamente, podemos crear un botón o cualquier otro elemento de la interfaz de usuario:
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()

Wow! Felicidades ¡Has creado un botón!
archivos .kv
Sin embargo, hay otra forma de crear elementos de la interfaz de usuario. Primero, implementamos nuestro formulario:
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()
Luego, creamos el archivo "worm.kv".
worm.kv
<Form>: but2: but_id Button: id: but_id pos: (200, 200)
¿Qué acaba de pasar? Creamos otro botón y asignamos id but_id. Luego, but_id coincidió con but2 del formulario. Significa que ahora podemos referirnos a este botón por 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"

Gráficos
Lo que hacemos a continuación es crear un elemento gráfico. Primero, lo implementamos en worm.kv:
<Form>: <Cell>: canvas: Rectangle: size: self.size pos: self.pos
Vinculamos la posición del rectángulo a self.pos y su tamaño a self.size. Entonces, ahora esas propiedades están disponibles en Cell, por ejemplo, una vez que creamos una celda, podemos hacer
class Cell(Widget): def __init__(self, x, y, size): super().__init__() self.size = (size, size)

Ok, hemos creado una celda.
Métodos innecesarios
Intentemos moverlo. Para hacer eso, debemos agregar la función Form.update y programarla.
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()
La celda se moverá a través del formulario. Como puede ver, podemos programar cualquier función con Clock.
A continuación, hagamos un evento táctil. Reescribir formulario:
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)
Cada touch_down crea una celda con coordenadas = (touch.x, touch.y) y un tamaño de 30. Luego, lo agregamos como un widget del formulario Y a nuestra propia matriz (para acceder fácilmente a ellos).
Ahora puede tocar su formulario y generar celdas.

Configuraciones ordenadas
Debido a que queremos obtener una buena serpiente, debemos distinguir las posiciones gráficas y las posiciones reales de las celdas.
Por quéMuchas razones para hacerlo. Toda la lógica debe estar conectada con los llamados datos reales, mientras que los datos gráficos son el resultado de los datos reales. Por ejemplo, si queremos hacer márgenes, la posición real de la celda será (100, 100) mientras que la posición gráfica del rectángulo - (102, 102).
PD: No lo haríamos si tratamos con on_draw clásico. Pero aquí no tenemos que programar on_draw.
Arreglemos el archivo worm.kv:
<Form>: <Cell>: canvas: Rectangle: size: self.graphical_size pos: self.graphical_pos
y 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) ...

El margen apareció para que se vea bonito, aunque creamos la segunda celda con X = 130 en lugar de 132. Más tarde, haremos un movimiento suave basado en la distancia entre actual_pos y graphical_pos.
Codificando el gusano
Implementación
Config de inicio en 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)
(Confía en mí, ¡te encantará!)
Luego, asigne config a la aplicación:
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
Reescribe init y comienza:
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)
Entonces, la célula:
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)
Con suerte, está más o menos claro.
y finalmente el gusano:
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)):
Demos vida a nuestro gusano.

Movimiento
Ahora lo haremos mover.
Es simple:
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)

Esta vivo! Esta vivo!
Controlando
Como podría juzgar por la imagen de vista previa, los controles de la serpiente serán los siguientes:

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)

Genial
Creando la fruta
Primero, lo inicializamos.
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)
El resultado actual:

Ahora deberíamos implementar algunos métodos de gusanos:
class Worm(Widget): ...
Otros beneficios de reunir_posicionesPor cierto, después de que implementamos reunir_posiciones, podemos cambiar fruit_dislocate:
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)
En este punto, la fruta no se ubicará en el azulejo del gusano.
... y agregue esta marca para actualizar ()
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()
Detección de auto golpe de baldosas
Queremos saber si la cabeza tiene la misma posición que una de las células del gusano.
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

Colorear, decorar y refactorizar códigos
Comencemos con la refactorización de código.
Reescribe y agrega
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 ...
Ahora, si el gusano está muerto (congelado), si tocas de nuevo, el juego se reiniciará.
Ahora vamos a decorar y colorear.
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
Reescribe 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()

Vamos a colorearlo. Reescribir Celda en .kv:
<Cell>: canvas: Color: rgba: self.color Rectangle: size: self.graphical_size pos: self.graphical_pos
Agregue esto a la celda .__ init__
self.color = (0.2, 1.0, 0.2, 1.0)
y esto a Form.start
self.fruit.color = (1.0, 0.2, 0.2, 1.0)
Genial, disfruta tu serpiente

Finalmente, haremos una etiqueta de "juego terminado"
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")
y haz que la celda de éxito sea roja:
en lugar de
def update(self, _): ... if self.worm_bite_self(): self.game_over() ...
escribir
def update(self, _): cell = self.worm_bite_self() if cell: cell.color = (1.0, 0.2, 0.2, 1.0) self.game_over()

¿Sigues prestando atención? Lo siguiente es la parte más interesante.
Sección de bonificación - movimiento suave
Debido a que el paso del gusano es igual al tamaño de la celda, no es tan suave. Pero queremos hacerlo avanzar con la mayor frecuencia posible, sin tener que reescribir toda la lógica del juego. Entonces, necesitamos crear un mecanismo que mueva nuestras poses gráficas pero no nuestras poses reales. Entonces, escribí un archivo simple:
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)
Para quienes no les gusta mi códigoEste módulo no es la solución más elegante ©. Es una mala solución y lo reconozco. Es una solución única en el mundo.
Entonces, simplemente cree smooth.py y copie y pegue este código en el archivo.
Finalmente, hagámoslo funcionar:
class Form(Widget): ... def __init__(self, config): ... self.smooth = smooth.XSmooth(["graphical_pos[0]", "graphical_pos[1]"])
Luego reemplazamos self.worm.move () con
class Form(Widget): ... def update(self, _): ... self.worm.move(self.cur_dir, smooth_motion=(self.smooth, self.config.INTERVAL))
Y así es como deberían verse los métodos de Cell
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)
Eso es todo, gracias por su atención!
Cómo funciona el resultado final:
El código finalmain.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
Código, ajustado por @tshirtmanRecibí algunos problemas con mi código, por ejemplo, tshirtman, uno de los contribuyentes del proyecto Kivy, me sugirió que no creara Cells como Widgets, sino que hiciera una instrucción Point. Sin embargo, no encuentro este código más fácil de entender que el mío, a pesar de que es definitivamente mejor en términos de interfaz de usuario y desarrollo de juegos. De todos modos, el código:
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
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
Siéntase libre de hacer cualquier pregunta.