مغناطيس الجواهر
الدرس 16 من 25 في دورة مشروع Pygame: حصّن القلعة على Coddy.
تطير الجواهر القريبة من الفارس نحوه، لذلك لا يضطر اللاعب إلى المشي فوق كل واحدة منها. تحرّك move_towards_ip(target, d) قيمة Vector2 مسافة تصل إلى d بكسل نحو نقطة، ولا تتجاوزها أبدًا:
def update(self, knight):
target = pygame.Vector2(knight.rect.center)
if self.pos.distance_to(target) < knight.magnet:
self.pos.move_towards_ip(target, 6)
self.rect.center = self.posالمسافة التي تبدأ عندها الجواهر بالطيران هي قيمة magnet الخاصة بالفارس، وتبلغ 80 بكسل في البداية؛ وستزيدها إحدى الترقيات.
تعطي spritecollide(sprite, group, True) كل كائنات المجموعة التي تلامس، كما أن True تزيلها من المجموعة أيضًا: سطر واحد يلتقط كل جوهرة يقف عليها الفارس.
التحدي
متوسطتطير الجواهر القريبة من الفارس نحوه ويلتقطها.
- امنح الفارس
magnet = 80، وامنحGemالدالةupdateالمذكورة أعلاه. - حمّل
gem_sound = sound("coin.ogg"). امنحNightالقيمةxp = 0والدالةcollect(self): تُزال كل جوهرة يلمسها الفارس، وتمنحه نقطة خبرة واحدة، ويُشغَّلgem_sound. - في
update: أضفself.gems.update(self.knight)بعد تحرّك المقذوفات، وأضفself.collect()بعدhurt_monsters.
اضغط «العب» وامشِ بالقرب من جوهرة: ستطير نحوك مع صوت قطعة نقدية. ثم اضغط «تحقق».
جرّب بنفسك
import random
import pygame
WIDTH, HEIGHT = 720, 480
TILE = 48
NIGHT = 60 * 60 * 4
# أنواع الوحوش التي ترسلها كل موجة، من الموجة الأولى إلى الأخيرة
WAVES = [
["slime"],
["slime", "bat"],
["slime", "bat", "spider"],
["bat", "spider"],
["slime", "spider", "ghost"],
["bat", "spider", "ghost"],
["spider", "ghost"],
["bat", "ghost"],
]
WHITE = (240, 240, 240)
BOLT = (120, 200, 255)
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Hold the Keep")
clock = pygame.time.Clock()
font = pygame.font.Font("assets/fonts/pixel.ttf", 24)
big_font = pygame.font.Font("assets/fonts/pixel.ttf", 48)
def load(name):
return pygame.image.load("assets/images/" + name).convert_alpha()
def sound(name):
return pygame.mixer.Sound("assets/sounds/" + name)
def whiten(image):
white = image.copy()
white.fill((255, 255, 255), special_flags=pygame.BLEND_RGB_MAX)
return white
floor_img = load("floor.png")
wall_img = load("wall.png")
knight_img = load("knight.png")
axe_img = load("axe.png")
gem_img = load("gem.png")
heart_img = load("heart.png")
empty_img = load("heart_empty.png")
MONSTERS = {
"slime": {"image": load("slime.png"), "speed": 1.0, "hp": 1},
"bat": {"image": load("dungeon_bat.png"), "speed": 1.6, "hp": 1},
"spider": {"image": load("spider.png"), "speed": 1.2, "hp": 3},
"ghost": {"image": load("ghost.png"), "speed": 0.8, "hp": 4},
}
hit_sound = sound("hit.ogg")
hurt_sound = sound("thud.ogg")
courtyard = pygame.Rect(TILE, TILE, WIDTH - 2 * TILE, HEIGHT - 2 * TILE)
background = pygame.Surface((WIDTH, HEIGHT))
for row in range(HEIGHT // TILE):
for col in range(WIDTH // TILE):
x = col * TILE
y = row * TILE
if courtyard.collidepoint(x, y):
background.blit(floor_img, (x, y))
else:
background.blit(wall_img, (x, y))
def draw(image, center):
screen.blit(image, image.get_rect(center=center))
def clock_text(frames):
seconds = frames // 60
return f"{seconds // 60}:{seconds % 60:02d}"
def edge_point():
side = random.randint(0, 3)
if side == 0:
return (random.randint(0, WIDTH), -24)
if side == 1:
return (random.randint(0, WIDTH), HEIGHT + 24)
if side == 2:
return (-24, random.randint(0, HEIGHT))
return (WIDTH + 24, random.randint(0, HEIGHT))
class Knight(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = knight_img
self.rect = pygame.Rect(0, 0, 24, 24)
self.rect.center = courtyard.center
self.speed = 3
self.hearts = 5
self.max_hearts = 5
self.hurt = 0
self.bolt_wait = 30
self.bolt_damage = 1
self.axes = 1
def move(self, keys):
if keys[pygame.K_LEFT]:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT]:
self.rect.x += self.speed
if keys[pygame.K_UP]:
self.rect.y -= self.speed
if keys[pygame.K_DOWN]:
self.rect.y += self.speed
self.rect.clamp_ip(courtyard)
class Monster(pygame.sprite.Sprite):
def __init__(self, kind, pos):
super().__init__()
self.kind = kind
self.image = MONSTERS[kind]["image"]
self.speed = MONSTERS[kind]["speed"]
self.hp = MONSTERS[kind]["hp"]
self.axe_wait = 0
self.flash = 0
self.pos = pygame.Vector2(pos)
self.rect = pygame.Rect(0, 0, 28, 28)
self.rect.center = self.pos
def update(self, target):
step = pygame.Vector2(target) - self.pos
if step.length() > 0:
self.pos += step.normalize() * self.speed
self.rect.center = self.pos
if self.axe_wait > 0:
self.axe_wait -= 1
if self.flash > 0:
self.flash -= 1
def hit(self, damage):
self.hp -= damage
self.flash = 6
hit_sound.play()
class Bolt(pygame.sprite.Sprite):
def __init__(self, start, target, damage):
super().__init__()
self.pos = pygame.Vector2(start)
step = pygame.Vector2(target) - self.pos
if step.length() == 0:
step = pygame.Vector2(1, 0)
self.velocity = step.normalize() * 7
self.damage = damage
self.rect = pygame.Rect(0, 0, 10, 10)
self.rect.center = self.pos
def update(self):
self.pos += self.velocity
self.rect.center = self.pos
if not screen.get_rect().colliderect(self.rect):
self.kill()
class Gem(pygame.sprite.Sprite):
def __init__(self, pos):
super().__init__()
self.rect = pygame.Rect(0, 0, 24, 24)
self.rect.center = pos
self.rect.clamp_ip(courtyard)
self.pos = pygame.Vector2(self.rect.center)
class Night:
def __init__(self):
self.knight = Knight()
self.time = 0
self.monsters = pygame.sprite.Group()
self.bolts = pygame.sprite.Group()
self.bolt_timer = 0
self.spin = 0
self.gems = pygame.sprite.Group()
self.state = "playing"
def wave(self):
return min(len(WAVES), self.time * len(WAVES) // NIGHT + 1)
def spawn(self):
wave = self.wave()
if self.time % max(16, 46 - wave * 5) == 0:
kind = random.choice(WAVES[wave - 1])
self.monsters.add(Monster(kind, edge_point()))
def nearest(self):
point = pygame.Vector2(self.knight.rect.center)
best = None
for monster in self.monsters:
if best is None or point.distance_to(monster.pos) < point.distance_to(best.pos):
best = monster
return best
def shoot(self):
self.bolt_timer += 1
target = self.nearest()
if self.bolt_timer >= self.knight.bolt_wait and target is not None:
self.bolt_timer = 0
bolt = Bolt(self.knight.rect.center, target.pos, self.knight.bolt_damage)
self.bolts.add(bolt)
def axe_spots(self):
spots = []
for i in range(self.knight.axes):
angle = self.spin + i * 360 / self.knight.axes
offset = pygame.Vector2(70, 0).rotate(angle)
spots.append(pygame.Vector2(self.knight.rect.center) + offset)
return spots
def hurt_monsters(self):
for bolt in self.bolts:
target = pygame.sprite.spritecollideany(bolt, self.monsters)
if target is not None:
target.hit(bolt.damage)
bolt.kill()
for spot in self.axe_spots():
blade = pygame.Rect(0, 0, 36, 36)
blade.center = spot
for monster in self.monsters:
if monster.axe_wait == 0 and blade.colliderect(monster.rect):
monster.hit(1)
monster.axe_wait = 20
for monster in list(self.monsters):
if monster.hp <= 0:
monster.kill()
self.gems.add(Gem(monster.pos))
def take_hits(self):
knight = self.knight
if knight.hurt > 0:
knight.hurt -= 1
elif pygame.sprite.spritecollideany(knight, self.monsters):
knight.hearts -= 1
knight.hurt = 75
hurt_sound.play()
if knight.hearts <= 0:
self.state = "lost"
def update(self, keys):
self.time += 1
self.spin = (self.spin + 4) % 360
self.knight.move(keys)
self.spawn()
self.monsters.update(self.knight.rect.center)
self.bolts.update()
self.shoot()
self.hurt_monsters()
self.take_hits()
night = Night()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if night.state == "playing":
night.update(pygame.key.get_pressed())
screen.blit(background, (0, 0))
knight = night.knight
for gem in night.gems:
draw(gem_img, gem.rect.center)
for monster in night.monsters:
if monster.flash > 0:
draw(whiten(monster.image), monster.rect.center)
else:
draw(monster.image, monster.rect.center)
if knight.hurt % 10 < 5:
draw(knight.image, knight.rect.center)
for spot in night.axe_spots():
draw(pygame.transform.rotate(axe_img, -night.spin * 3), spot)
for bolt in night.bolts:
pygame.draw.circle(screen, BOLT, bolt.rect.center, 5)
for i in range(knight.max_hearts):
if i < knight.hearts:
screen.blit(heart_img, (8 + i * 34, 6))
else:
screen.blit(empty_img, (8 + i * 34, 6))
left = font.render(clock_text(max(0, NIGHT - night.time)) + " until dawn", True, WHITE)
screen.blit(left, left.get_rect(center=(WIDTH // 2, 24)))
if night.state == "lost":
message = big_font.render("The keep has fallen", True, WHITE)
screen.blit(message, message.get_rect(center=(WIDTH // 2, 200)))
pygame.display.flip()
clock.tick(60)
pygame.quit()
جميع دروس مشروع Pygame: حصّن القلعة
تدرّب بنفسك: Playground لـ Pygame