Здесь что-то вроде новичка в Python, и это один из моих первых «больших» (больших для новичков, таких как я) проектов, которые я предпринял с Ткинтер и Пынпут. По сути, этот код будет имитировать модель движения врага на основе некоторых условий, которые я создал (вы сможете увидеть, как различные «фазы» распечатываются на консоли). Затем вы можете управлять игроком с помощью клавиш со стрелками.
Я хотел бы посоветовать, что мне следует улучшить в будущих проектах. Стоит добавить еще комментарии? Хорошо ли структурирован код? и т.п.
import math
import tkinter as tk
from pynput import keyboard
class Application:
def __init__(self, master, height = 800, width = 800, updatesPerSecond = 10, safeCircle = True):
self.height = height
self.width = width
self.root = master
self.updatesPerSecond = updatesPerSecond
self.player = Player()
self.enemy = Enemy()
self.safeCircle = safeCircle
self.canvas = tk.Canvas(self.root, height = self.height, width = self.width)
self.canvas.pack()
self.player_rectangle = self.canvas.create_rectangle(self.player.x-self.player.hLength, self.player.y-self.player.hLength, self.player.x+self.player.hLength, self.player.y+self.player.hLength)
self.enemy_rectangle = self.canvas.create_rectangle(self.enemy.x-self.player.hLength, self.enemy.y-self.player.hLength, self.enemy.x+self.player.hLength, self.enemy.y+self.player.hLength)
if self.safeCircle:
self.safe_circle = self.canvas.create_oval(self.player.x-self.enemy.safe_distance, self.player.y-self.enemy.safe_distance, self.player.x+self.enemy.safe_distance, self.player.y+self.enemy.safe_distance)
self.keypress_list = []
self.listener = keyboard.Listener(on_press = self.on_press, on_release = self.on_release)
self.listener.start()
self.player_movement()
self.enemy_movement()
def player_movement(self):
if "down" in self.keypress_list:
self.player.update_y(self.player.speed)
if "up" in self.keypress_list:
self.player.update_y(-self.player.speed)
if "left" in self.keypress_list:
self.player.update_x(-self.player.speed)
if "right" in self.keypress_list:
self.player.update_x(self.player.speed)
self.player.boundary_check(self.height, self.width)
self.canvas.coords(self.player_rectangle, self.player.x-self.player.hLength, self.player.y-self.player.hLength, self.player.x+self.player.hLength, self.player.y+self.player.hLength)
if self.safeCircle:
self.canvas.coords(self.safe_circle, self.player.x-self.enemy.safe_distance, self.player.y-self.enemy.safe_distance, self.player.x+self.enemy.safe_distance, self.player.y+self.enemy.safe_distance)
self.root.after(1000//self.updatesPerSecond, self.player_movement)
def enemy_movement(self):
self.enemy.update_pos(self.player)
self.enemy.boundary_check(self.height, self.width)
self.canvas.coords(self.enemy_rectangle, self.enemy.x-self.enemy.length/2, self.enemy.y-self.enemy.length/2, self.enemy.x+self.enemy.length/2, self.enemy.y+self.enemy.length/2)
self.root.after(1000//self.updatesPerSecond, self.enemy_movement)
def key_test(self, key):
try:
return key.name
except:
return
def on_press(self, key):
key = self.key_test(key)
if not key in self.keypress_list:
self.keypress_list.append(key)
def on_release(self, key):
key = self.key_test(key)
self.keypress_list.remove(key)
class SimObject:
def __init__(self, x, y, speed, length):
self.x = x
self.y = y
self.speed = speed
self.length = length
self.hLength = self.length/2
def boundary_check(self, height, width):
if self.x - self.hLength < 0:
self.x = self.hLength
if self.y - self.hLength < 0:
self.y = self.hLength
if self.x + self.hLength > width:
self.x = width - self.hLength
if self.y + self.hLength > height:
self.y = height - self.hLength
def update_x(self, offset):
self.x+=offset
def update_y(self, offset):
self.y+=offset
class Player(SimObject):
def __init__(self, x = 400, y = 400, speed = 10, length = 20):
super().__init__(x, y, speed, length)
class Enemy(SimObject):
def __init__(self, x = 10, y = 10, speed = 5, length = 20, safe_distance = 100):
super().__init__(x, y, speed, length)
self.safe_distance = safe_distance
self.last_phase = -1
def update_phase(self, n):
phase_list=[f"{i} Phase" for i in ["Orbit", "Rush", "Run"]]
if self.last_phase!=n:
print(phase_list[n])
self.last_phase = n
def update_pos(self, player):
PI=math.pi
dx=player.x-self.x
dy=player.y-self.y
g_to_p_ang=math.atan2(dy,dx)
p_to_g_ang=PI+g_to_p_ang
dist=math.sqrt(dx*dx+dy*dy)
ang_increase=self.speed/self.safe_distance
t=p_to_g_ang
if abs(dist-self.safe_distance)<=self.speed:#near the orbit
self.update_phase(0)
t+=ang_increase
self.x=self.safe_distance*math.cos
self.y=self.safe_distance*math.sin
elif dist>self.safe_distance:#far from orbit
self.update_phase(1)
self.update_x(self.speed*math.cos(g_to_p_ang))
self.update_y(self.speed*math.sin(g_to_p_ang))
elif dist<self.safe_distance:#far inside of orbit
self.update_phase(2)
self.update_x(self.speed*math.cos(p_to_g_ang))
self.update_y(self.speed*math.sin(p_to_g_ang))
root = tk.Tk()
root.resizable(0,0)
root.title("Enemy Movement Test")
application = Application(root)
tk.mainloop()
2 ответа
Отформатируйте свой код в соответствии с PEP-8, для этого есть автоматическая проверка и даже автоматические форматеры.
Это часто считается запахом кода:
def key_test(self, key):
try:
return key.name
except:
return
Видеть: https://stackoverflow.com/questions/10594113/bad-idea-to-catch-all-exceptions-in-python
- Некоторые методы длиннее, чем я считаю удобочитаемыми, а некоторые содержат повторяющийся код. Попробуйте выделить повторяющийся блок кода в качестве методов и несколько повторяющихся выражений в виде хорошо названных локальных переменных, чтобы объяснить процесс.
Помимо того, что предлагает Роман Павелка:
- Представьте свой
keypress_list
естьkeypresses
set (обратите внимание, что вставлять тип переменной в ее имя бесполезно; это то, для чего нужны подсказки типа) - Выделите процедуры расчета прямоугольника с маржей в свои
SimObject
учебный класс - Не запускайте ничего в своем конструкторе. ‘start’ в отдельной подпрограмме или, возможно, в методе входа диспетчера контекста, останавливаясь в соответствующей подпрограмме выхода.
- Не назначайте логическое значение
self.safeCircle
; это должно бытьOptional
(т.е. объект илиNone
) - Не запускайте отдельные таймеры для объектов врага и игрока; просто используйте один
key_test
следует использоватьgetattr
сNone
default, который позволит достичь того же эффекта более явным и безопасным способом- Добавить подсказки типа PEP484
- Не добавляйте имя клавиши в набор нажатий клавиш, если имя клавиши
None
- Перефразируй свой
boundary_check
— который вообще не проверяет (ничего не возвращается), поэтому его следует назвать как-то вродеenforce_bounds
— как сериюmin
иmax
звонки - Представлять
last_phase
как перечисление для лучшей ремонтопригодности и удобочитаемости - Нет необходимости импортировать
pi
если вы просто инвертируете дельты координат в зависимости от того, находитесь ли вы в фазе спешки или бега - В
update_pos
, Твоя последняяelse
не нуждается в условиях; это избыточно - Переместите логику внизу в основную защиту
Предложенный
import enum
import math
import tkinter as tk
from enum import Enum
from typing import Optional, Tuple
from pynput import keyboard
from pynput.keyboard import Key
class Application:
def __init__(
self, master: tk.Tk, height: int = 800, width: int = 800,
updates_per_second: int = 10, safe_circle: bool = True,
):
self.height = height
self.width = width
self.root = master
self.updates_per_second = updates_per_second
self.player = Player()
self.enemy = Enemy()
self.canvas = tk.Canvas(self.root, height=self.height, width=self.width)
self.canvas.pack()
self.player_rectangle = self.canvas.create_rectangle(*self.player.rect)
self.enemy_rectangle = self.canvas.create_rectangle(*self.enemy.rect)
if safe_circle:
self.safe_circle = self.canvas.create_oval(
*self.player.margin_rect(self.enemy.safe_distance)
)
else:
self.safe_circle = None
self.keypresses = set()
self.listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
def __enter__(self) -> 'Application':
self.listener.start()
self.movement()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.listener.stop()
def movement(self) -> None:
self.player_movement()
self.enemy_movement()
self.root.after(1000 // self.updates_per_second, self.movement)
def player_movement(self) -> None:
if "down" in self.keypresses:
self.player.update_y(self.player.speed)
if "up" in self.keypresses:
self.player.update_y(-self.player.speed)
if "left" in self.keypresses:
self.player.update_x(-self.player.speed)
if "right" in self.keypresses:
self.player.update_x(self.player.speed)
self.player.enforce_bounds(self.height, self.width)
self.canvas.coords(self.player_rectangle, *self.player.rect)
if self.safe_circle:
self.canvas.coords(
self.safe_circle,
*self.player.margin_rect(self.enemy.safe_distance)
)
def enemy_movement(self) -> None:
self.enemy.update_pos(self.player.x, self.player.y)
self.enemy.enforce_bounds(self.height, self.width)
self.canvas.coords(
self.enemy_rectangle,
*self.enemy.margin_rect(self.enemy.length / 2),
)
@staticmethod
def key_test(key: Key) -> Optional[str]:
return getattr(key, 'name', None)
def on_press(self, key: Key) -> None:
key = self.key_test(key)
if key is not None:
self.keypresses.add(key)
def on_release(self, key: Key) -> None:
key = self.key_test(key)
self.keypresses.discard(key)
@enum.unique
class EnemyPhase(Enum):
ORBIT = 'Orbit'
RUSH = 'Rush'
RUN = 'Run'
class SimObject:
def __init__(self, x: int, y: int, speed: int, length: int):
self.x = x
self.y = y
self.speed = speed
self.length = length
self.h_length = self.length / 2
def enforce_bounds(self, height: int, width: int) -> None:
self.x = max(self.h_length, min(width - self.h_length, self.x))
self.y = max(self.h_length, min(height - self.h_length, self.y))
def update_x(self, offset: float) -> None:
self.x += offset
def update_y(self, offset: float) -> None:
self.y += offset
def margin_rect(self, margin: float) -> Tuple[float, float, float, float]:
return (
self.x - margin, self.y - margin,
self.x + margin, self.y + margin,
)
@property
def rect(self) -> Tuple[float, float, float, float]:
return self.margin_rect(self.h_length)
class Player(SimObject):
def __init__(self, x: int = 400, y: int = 400, speed: int = 10, length: int = 20):
super().__init__(x, y, speed, length)
class Enemy(SimObject):
def __init__(
self, x: int = 10, y: int = 10, speed: int = 5, length: int = 20,
safe_distance: int = 100,
):
super().__init__(x, y, speed, length)
self.safe_distance = safe_distance
self.last_phase = EnemyPhase.RUSH
def update_phase(self, phase: EnemyPhase) -> None:
if self.last_phase != phase:
self.last_phase = phase
print(f'{phase.value} Phase')
def update_pos(self, player_x: float, player_y: float) -> None:
dx = self.x - player_x
dy = self.y - player_y
p_to_g_ang = math.atan2(dy, dx)
dist = math.sqrt(dx*dx + dy*dy)
if abs(dist - self.safe_distance) <= self.speed: # near the orbit
ang_increase = self.speed / self.safe_distance
t = p_to_g_ang + ang_increase
self.update_phase(EnemyPhase.ORBIT)
self.x = self.safe_distance * math.cos
self.y = self.safe_distance * math.sin
else:
sx = self.speed * math.cos(p_to_g_ang)
sy = self.speed * math.sin(p_to_g_ang)
if dist > self.safe_distance: # far from orbit
self.update_phase(EnemyPhase.RUSH)
self.update_x(-sx)
self.update_y(-sy)
else: # far inside of orbit
self.update_phase(EnemyPhase.RUN)
self.update_x(sx)
self.update_y(sy)
def main():
root = tk.Tk()
root.resizable(0, 0)
root.title("Enemy Movement Test")
with Application(root):
tk.mainloop()
if __name__ == '__main__':
main()
Спасибо за обзор! Поскольку я только что запустил Python не так давно, у меня все еще есть проблемы с пониманием многих частей предложенного вами кода, поэтому мне нужно время, чтобы понять его. А пока я просто хочу спросить, нужно ли мне устанавливать
enum
иtyping
модулей, и не могли бы вы более подробно объяснить, что делают эти модули?— Эйден Чоу
Оба модуля встроены; они у вас уже есть.
typing
добавляет подсказки, не относящиеся к среде выполнения, для программистов и статических анализаторов, чтобы лучше понять ваш код. Перечисления используются для представления переменной, которая может принимать ограниченный набор значений.— Райндериен