Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

python:pygameで生態系[第二章]動物編

注意!
この記事は前回の記事を読んでいることを前提に作られています。
まだ読んでいない方は下のリンクからどうぞ。

python:pygameで生態系[第一章]植物編

🔗 目次

  1. まえがき
  2. この章での完成品
  3. 動物の基底クラスを作る
  4. 草食動物を作る
  5. 肉食動物を作る
  6. あとがき

🔗 まえがき

皆さんどうもこんにちは、株式会社ECN所属インターンのFuseです。
前回は植物と養分を実装して、生やしたり枯らしたりしました。
このままでは植物しかいないので、画面をうろつく動物を今日は実装していきます。

この章で実装する内容

  • 最初にこれまでの内容に加え、動物が配置される
  • 動物はうろつきながらものを食べたり、外敵から逃げたりする
  • 動物は所持エネルギーが一定以上になると分裂する
  • 餓死した動物は、近くに養分を所持しているエネルギーに応じ残す
  • 養分の総量は決して変わらない

↑目次に戻る

🔗 この章での完成品


↑目次に戻る

🔗 動物の基底クラスを作る

まずは動物たちの基底クラスであるBaseAnimalクラスを作りましょう。

actor/baseanimal.py

import pygame
import random
from actor.baseactor import pygame
from actor.baselife import BaseLife


class BaseAnimal(BaseLife):
    def __init__(
        self, x: int, y: int, maxhealth: int, energy: int, sight: int, speed: int
    ) -> None:
        self.vec = pygame.math.Vector2(1, 1).rotate(random.random() * 360)  # 移動方向ベクトル
        self.sight: int = sight       # 視界の半径
        self.speed: float = speed     # 移動速度(最大)
        self.speedgear: float = 1     # 移動速度倍率
        self.target: BaseLife = None  # 追いかけている対象
        super().__init__(x, y, maxhealth, energy)

    def step(self, screen: pygame.surface, actors: list) -> None:
        self.health -= self.speedgear  # 速度倍率に応じ体力減少

        if self.pos.y <= 20 and self.vec.y < 0:
            self.vec.y *= -1
        if self.pos.y >= 940 and self.vec.y > 0:
            self.vec.y *= -1
        if self.pos.x <= 20 and self.vec.x < 0:
            self.vec.x *= -1
        if self.pos.x >= 1260 and self.vec.x > 0:
            self.vec.x *= -1
        self.pos += self.vec.normalize() * self.speedgear * self.speed
        return super().step(screen)

    def get_insight(self, target: type, actors: list):
        # 視界内のtarget型のアクタのリストを返す
        return list(
            filter(
                lambda x: (self.pos.x - x.pos.x) ** 2 + (self.pos.y - x.pos.y) ** 2
                <= self.sight**2
                and isinstance(x, target),
                actors,
            )
        )

前回作ったBaseLifeクラスのプロパティに視界と移動速度とその倍率、追跡対象と移動方向ベクトルを追加。
メソッドに視界内の指定タイプのアクタを取得するget_insightを追加しました。
step内には画面外に出れないようにする処理を追加しています。
また、speedgearに応じて追加でヘルスを減少させることによって、
歩きや走り、休眠を使い分ける生き物っぽい動きを作れそうです。

↑目次に戻る

🔗 草食動物を作る

次に作るのが草食動物。草を食べてエネルギーを貯め、一定数エネルギーがたまるとエネルギーを消費して分裂します。

actor/sheep.py

import pygame
from actor.baseanimal import BaseAnimal


class Sheep(BaseAnimal):
    def __init__(self, x: int, y: int) -> None:
        super().__init__(x, y, 200, 4, 200, 1.5)
        self.speedgear = 0.5
        self.escapetime = 0  # 逃亡状態が解除される時間
        self.vdangervec = pygame.Vector2(0, 0)

    def step(self, screen: pygame.surface, actors: list):
        if self.time > 45:  # 重なり防止のため、最初の1.5秒はランダム方向に直進させる
            from actor.wolf import Wolf
            from actor.plant import Plant

            self.speedgear = 0.8
            danger = self.get_insight(Wolf, actors)  # 視界内の天敵のリスト
            verydanger = filter(
                lambda x: x.target == self, self.get_insight(Wolf, actors)
            )  # 自分を狙う天敵のリスト
            food = self.get_insight(Plant, actors)  # 視界内の飯のリスト
            dangervec = pygame.Vector2(0, 0)
            foodvec = pygame.Vector2(0, 0)
            for v in danger:
                dangervec += (self.pos - v.pos) * (
                    self.sight / max(1, (v.pos - self.pos).magnitude())
                )
            if dangervec.length() > 0:
                dangervec = dangervec.normalize()
            for v in verydanger:
                self.vdangervec += self.pos - v.pos
                self.escapetime = self.time + 15  # 逃亡開始
            if self.escapetime > self.time:
                self.speedgear = 1
            else:
                self.vdangervec = pygame.Vector2(0, 0)  # 逃亡終了
            if self.vdangervec.length() > 0:
                self.vdangervec = self.vdangervec.normalize() * 5
            if len(food) > 0:
                food.sort(key=lambda x: (self.pos - x.pos).length())  # 距離が短い順に並べる
                foodvec += (food[0].pos - self.pos) * (
                    self.sight / max(1, (food[0].pos - self.pos).magnitude())
                )
                self.target = food[0]
                foodvec = foodvec.normalize() * 3
                if self.target != None:  # 捕食対象を追いかけているなら
                    if (self.pos - self.target.pos).length() < 20:  # 至近距離なら
                        self.energy += self.target.energy
                        self.target.eaten()
                        self.heal(300)
                        if self.energy >= 8:  # 十分なエネルギーがあるなら
                            while self.energy >= 8:
                                self.energy -= 4
                                actors.append(Sheep(self.pos.x, self.pos.y))
            if (foodvec + dangervec + self.vdangervec).length() > 0:
                self.vec = (foodvec + dangervec + self.vdangervec).normalize()
        pygame.draw.rect(
            screen, (255, 255, 255), (self.pos.x - 20, self.pos.y - 20, 40, 40)
        )
        super().step(screen, actors)

草食動物(■)は植物を食べるだけでなく、外敵である肉食動物(🔶)から逃げなければなりません。
視界内のアクタを捕食対象(food)、外敵(danger)、至急逃げないとヤバい外敵(verydanger)の3つに分け、
それぞれとの距離がどれだけ近いかを重みとして方向ベクタに加算しています。
そして一定距離以内の捕食対象を自動で捕食し、エネルギーがたまったら分裂。
そんなコードになっています。

↑目次に戻る

🔗 肉食動物を作る

最後に、草食動物を食べる肉食動物を作っていきましょう。

actor/wolf.py

import pygame
from actor.baseanimal import *


class Wolf(BaseAnimal):
    def __init__(self, x: int, y: int) -> None:
        super().__init__(x, y, 350, 12, 400, 4)
        self.speedgear = 0.5

    def step(self, screen: pygame.surface, actors: list):
        from actor.sheep import Sheep

        if self.time > 45:
            self.speedgear = 0.3
            food = self.get_insight(Sheep, actors)  # 視界内の飯のリスト
            foodvec = pygame.Vector2(0, 0)
            if self.health / max(self.maxhealth, 0.1) < 1 and len(food) > 0:
                food.sort(key=lambda x: (self.pos - x.pos).length())
                foodvec += (food[0].pos - self.pos) * (
                    self.sight / max(1, (food[0].pos - self.pos).magnitude())
                )
                self.target = food[0]
                if foodvec.length() > 0:
                    foodvec = foodvec.normalize()
                    self.speedgear = 1.1
                    if self.target != None:
                        if (self.pos - self.target.pos).length() < 20:
                            self.energy += self.target.energy
                            self.target.eaten()
                            self.heal(300)
                            if self.energy >= 24:
                                while self.energy >= 24:
                                    self.energy -= 12
                                    actors.append(Wolf(self.pos.x, self.pos.y))
            if (foodvec).length() > 0:
                self.vec = (foodvec).normalize()
        pygame.draw.polygon(
            screen,
            (255, 255, 255),
            (
                self.pos + (20, 0),
                self.pos + (0, 20),
                self.pos + (-20, 0),
                self.pos + (0, -20),
            ),
        )
        super().step(screen, actors)

肉食動物はもっと簡単です。草食動物から逃亡処理がなくなりました。
あとはmain.pyの初期配置部分に動物を配置する処理を加えれば完成です。

↑目次に戻る

🔗 あとがき

どうでしたか?動物が増えて、一気ににぎやかになりましたね。
次回は、こんな動物たちに一体ごとに個性をつけていきたいと思います。
それでは、またいつか発展編でお会いしましょう。

↑目次に戻る


株式会社ECNはPHP、JavaScriptを中心にお客様のご要望に合わせたwebサービス、システム開発を承っております。
ビジネスの最初から最後までサポートを行い
お客様のイメージに合わせたWebサービス、システム開発、デザインを行います。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。