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

python:pygameで生態系[第三章]発展編

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

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

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

🔗目次

  1. まえがき
  2. この章での完成品
  3. ソースコード
  4. 基底クラスに手を加える
  5. 植物をカラフルにする
  6. 動物をカラフルにする
  7. あとがき
  8. 参考動画

🔗 まえがき

皆さんどうもこんにちは、株式会社ECN所属インターンのFuseです。
前回は動物を実装して、喰ったり喰わせたりしました。
これで一区切り完成はしているのですが、せっかくなので生き物たちに個体ごとの個性をつけたいと思います。

色を使ったアイデアには、こちらの動画シリーズを参考にしました。

この章で実装する内容

  • 動物・植物問わず、生物は「色」を持つ。
  • 色に応じてパラメーターに倍率がかかる。
  • 分裂時、RGBの割り振りがランダムに変化する
  • RGB値の合計は常に変わらない

↑目次に戻る

🔗 この章での完成品


↑目次に戻る

🔗 ソースコード

Fuses-Garage/LifeSimulator - GitHub

↑目次に戻る

🔗 基底クラスに手を加える

actor/baselife

import pygame
import random
from actor.baseactor import *


class BaseLife(BaseActor):
    # 初期化関数の引数にcolorが追加されています。
    def __init__(
        self, x: int, y: int, maxhealth: int, energy=1, color=(85, 85, 85)
    ) -> None:
        self.maxhealth = maxhealth
        self.health = maxhealth
        self.energy = energy
        self.color = color
        super().__init__(x, y)

    def step(self, screen: pygame.surface) -> None:
        self.health -= 0.3
        self.maxhealth -= 0.2
        if self.health <= 0:
            self.die()
        super().step(screen)

    def heal(self, value: int):
        self.health = min(self.health + value, self.maxhealth)

    def die(self):
        self.health = 0
        self.maxhealth = 0
        self.alive = False

    def eaten(self):
        self.energy = 0
        self.die()

    # ここから追加分です
    def calc_first_color(self):
        r = 0
        g = 0
        b = 0
        for _ in range(400):
            rand = random.randint(0, 2)
            if rand == 0:
                r += 1
            elif rand == 1:
                g += 1
            elif rand == 2:
                b += 1
        return (r, g, b)

    def calc_next_color(self, parentcol: tuple):
        r = parentcol[0] - 10
        g = parentcol[1] - 10
        b = parentcol[2] - 10
        for _ in range(30):
            rand = random.randint(0, 2)
            if rand == 0:
                r += 1
            elif rand == 1:
                g += 1
            elif rand == 2:
                b += 1
        return (r, g, b)

まずは生き物たちの基底クラスであるBaseLifeから。

プロパティに色を表すタプルを格納するcolorを追加し、
400ポイントを割り振って色の初期値を決定する関数calc_first_color
RGBそれぞれから10ポイント引いて30ポイントを振り直す親の色から子供の色を計算する関数calc_next_colorを作りました。
これで動植物問わず色を保持することができます。

↑目次に戻る

🔗 植物をカラフルにする

生き物に色を保持させることができたので、次はその色を植物の描画とパラメータに反映させていきます。

actor/plant.py

import pygame
import random
from actor.baselife import BaseLife
from actor.nourishment import Nourishment


class Plant(BaseLife):
    def __init__(self, x: int, y: int, color=None) -> None:
        if color == None:
            color = self.calc_first_color()  # 色が指定されてなければランダムに割り振る
        self.splittime = int(pygame.math.lerp(30, 5, color[0] / 255))  # 赤は増殖速度
        self.splitrange = int(pygame.math.lerp(400, 900, color[2] / 255))  # 青は増殖範囲
        super().__init__(
            x, y, pygame.math.lerp(50, 200, color[1] / 255), 1, color
        )  # 緑は体力

    def step(self, screen: pygame.surface, actors: list):
        pygame.draw.polygon(
            screen,
            self.color,
            (
                (self.pos.x - 20, self.pos.y + 30),
                (self.pos.x, self.pos.y - 40),
                (self.pos.x + 20, self.pos.y + 30),
            ),
        )  # 色を描画に反映
        self.splittime -= 1
        if self.splittime == 0:
            n = list(
                filter(
                    lambda x: isinstance(x, Nourishment)
                    and (self.pos.x - x.pos.x) ** 2 + (self.pos.y - x.pos.y) ** 2
                    <= self.splitrange**2,
                    actors,
                )
            )
            if len(n) > 0:
                item = n[random.randint(0, len(n) - 1)]
                item.alive = False
                actors.append(
                    Plant(item.pos.x, item.pos.y, self.calc_next_color(self.color))
                )  # 増殖の際、子は自分の色に近い色になる
            self.splittime = int(pygame.math.lerp(30, 5, self.color[0] / 255))
        super().step(screen)

次に植物がカラフルになれるようにしていきます。

  • 赤→移動速度
  • 緑→寿命兼腹持ち
  • 青→視界半径

といったように影響が出ます。
色の度合いによってパラメータを変える処理はpygame.math.lerpというおあつらえ向きの関数があるのでそちらを使います。

Lerpって?

線形補間に使われる関数で、a,b,tを指定すると、tの値に応じてaからbの間の値が返ってくる。
t<=0のときはaが返ってきますし1<=tのときはbが返ってきます。
0<t<1のときはa+(b-a)*(t)が返ってきます。

↑目次に戻る

🔗 動物をカラフルにする

植物の次は、動物にも同等の作業を行っていきます。

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,
        color=None,
    ) -> None:
        if color == None:
            color = self.calc_first_color()
        self.vec = pygame.math.Vector2(1, 1).rotate(random.random() * 360)  # 移動方向ベクトル
        self.sight: int = sight * pygame.math.lerp(0.75, 2.5, color[2] / 255)  # 視界の半径
        self.speed: float = speed * pygame.math.lerp(
            0.75, 3, color[0] / 255
        )  # 移動速度(最大)
        self.speedgear: float = 1     # 移動速度倍率
        self.target: BaseLife = None  # 追いかけている対象
        super().__init__(
            x, y, maxhealth * pygame.math.lerp(0.75, 2.5, color[1] / 255), energy, color
        )

    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,
            )
        )

まずは基底クラスであるBaseAnimalクラスに手を入れます。
動物では、

  • 赤→移動速度
  • 緑→寿命兼腹持ち
  • 青→視界半径

といったように影響が出ます。
動物ごとにパラメータが変わるので、植物とは違い渡された値に倍率で掛けてます。
それ以外は別に特筆することはないです。
何か書こうと思いましたが、書きたいものはすべて植物の解説に書いてしまいました…

actor/sheep.py

import pygame
from actor.baseanimal import BaseAnimal


class Sheep(BaseAnimal):
    def __init__(self, x: int, y: int, color=None) -> None:
        super().__init__(x, y, 200, 4, 200, 1.5, color)
        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,
                                        self.calc_next_color(self.color),
                                    )
                                )
            if (foodvec + dangervec + self.vdangervec).length() > 0:
                self.vec = (foodvec + dangervec + self.vdangervec).normalize()
        pygame.draw.rect(screen, self.color, (self.pos.x - 20, self.pos.y - 20, 40, 40))
        super().step(screen, actors)

actor/wolf.py

import pygame
from actor.baseanimal import *


class Wolf(BaseAnimal):
    def __init__(self, x: int, y: int, color=None) -> None:
        super().__init__(x, y, 400, 20, 200, 1.5, color)
        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.0
                    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 >= 40:
                                while self.energy >= 40:
                                    self.energy -= 20
                                    actors.append(
                                        Wolf(
                                            self.pos.x,
                                            self.pos.y,
                                            self.calc_next_color(self.color),
                                        )
                                    )
            if (foodvec).length() > 0:
                self.vec = (foodvec).normalize()
        pygame.draw.polygon(
            screen,
            self.color,
            (
                self.pos + (20, 0),
                self.pos + (0, 20),
                self.pos + (-20, 0),
                self.pos + (0, -20),
            ),
        )
        super().step(screen, actors)

最後に動物の各クラスに手を入れていきます。
やることは同じなので2ファイル同時に解説します。
やることは大きく分けて3つくらい。

  • __init__の引数にcolorを追加しsuper().__init__にバケツリレー
  • 描画時にself.colorの値を使うようにする
  • 分裂時にself.colorを基にランダムで変化した値を子の生成時に渡す

うち2つは植物と同じですが、逆に言えばこれだけお手軽な手段で生き物に多様性を持たせられるというわけです。
とにかく、これで生き物たちがカラフルかつ多種多様になりました!

↑目次に戻る

🔗 あとがき

どうでしたか?多種多様な動物たちが群れを作って動き回る姿は愛らしいものがありますね。
ソースコードも配布しておりますので、皆さんもぜひ改造して遊んでみてはいかがでしょうか?
それでは、また別の記事でお会いしましょう!
↑目次に戻る

🔗 参考動画群

↑目次に戻る


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


CONTACT

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