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

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

🔗目次

  1. まえがき
  2. この章での完成品
  3. 環境紹介
  4. 必要な物の整理
  5. 植物のライフサイクルを実装する
  6. あとがき

🔗 まえがき

皆さんどうもこんにちは、株式会社ECN所属インターン、Fuseです。皆さんは生物観察、お好きですか?
かくいう私も昔はアリとかよく観察してたものですし、人工生命に関する本を読み漁ったり、様々な動画を見たりしていたものです。
それで今日はといいますと、Pygameを使って架空世界にちょっとした生態系を作りたいと思います。
…といっても全部一気にやると記事の長さがものすごいことになりそうなので植物編、動物編、発展編の3章に分けていこうと思います。

Pygameって?

Pygameはゲーム制作に使われるPython用ライブラリで、
音声の再生や画像の回転・描画といった必須クラスの機能から
LerpやVectorといった便利な代物まで一通り揃ってる便利なライブラリです。

pygame/pygame - GitHub

この章で実装する内容

  • 最初に多量の養分と少量の植物が配置される
  • 植物は一定時間ごとに近くの養分を植物に置き換える
  • 植物は一定時間たつと枯れ、近くに養分を1つ残す
  • 養分の総量は決して変わらない

↑目次に戻る

🔗この章での完成品

↑目次に戻る

🔗 環境紹介

Python
3.11.5
pygame
2.5.1

↑目次に戻る

🔗必要なものの整理

栄養

動植物問わず、生物が生きていくには栄養が必要不可欠です。
一口に栄養といっても多種多様ですが、ここではひとまとめにして「栄養」とします。

イクラみたいですね。▲

植物

動物たちの食べ物としてまず必要になるのは「植物」ですね。
植物は様々な繁殖方法を持ちますが、この世界においては「栄養」の上に繁殖することにします。

▲草が生える範囲にも限界があるので、そこも実装していきます。

動物

植物だけでなく、文字通り動く者たちである「動物」も生態系には必要です。
動物は、

  • 草食動物
  • 肉食動物

この2種類を制作しようと思います。

↑目次に戻る

🔗 植物のライフサイクルを実装する

この章でできるもの

▲緑のとんがりコーンが植物、イクラが養分です。

コードの解説

actor/baseactor.py

import pygame


class BaseActor:
    def __init__(self, x: int, y: int) -> None:
        self.pos = pygame.math.Vector2(min(max(x, 0), 1280), min(max(y, 0), 960))  # 座標
        self.alive: bool = True  # アクターは生きているか?
        self.time = 0

    def step(self, screen: pygame.surface) -> None:
        self.time += 1
        pass

    def v2_to_v2int(self, v: pygame.math.Vector2):
        return (round(v.x.__float__()), round(v.y.__float__()))

動植物や養分のベースとなるBaseActorクラスには、プロパティにx、y座標と生死、メソッドに更新・描画処理であるstepを持たせています。

とはいえ基礎となるクラスなので描画処理はまだ空。最低限の実装です。

actor/nourishment.py

import pygame
from actor.baseactor import BaseActor


class Nourishment(BaseActor):
    def step(self, screen: pygame.surface, actors: list):
        pygame.draw.circle(screen, (255, 128, 0), (self.x, self.y), 5)  # イクラ描画

養分を表すクラスです。といっても変化点は描画部分のみです。

actor/baselife.py

import pygame
from actor.baseactor import *


class BaseLife(BaseActor):
    def __init__(self, x: int, y: int, maxhealth: int, energy=1) -> None:
        self.maxhealth = maxhealth
        self.health = maxhealth
        self.energy = energy
        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()

そしてこちらは動植物問わず生物のベースとなるクラス。
最大体力と体力と所持養分をプロパティに、
回復と死亡と被食をメソッドに追加しました。
また、最大HPをじわじわと減らすことで空腹とは別に寿命を表現しています。

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) -> None:
        self.splittime = random.randint(45, 75)
        self.splitrange = 750
        super().__init__(x, y, 500)

    def step(self, screen: pygame.surface, actors: list):
        pygame.draw.polygon(
            screen,
            (0, 255, 0),
            (
                (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.splittime = random.randint(30, 90)
        super().step(screen)

そしてようやく植物クラスが登場です。
ランダムな時間ごとに一定範囲内の養分をランダムに植物にします。
filterlambdaがとても便利ですね。

main.py

import sys, random
import pygame
from actor.baselife import BaseLife
from actor.plant import Plant
from actor.nourishment import Nourishment
from pygame.locals import *

pygame.init()  # pygame初期化
clock = pygame.time.Clock()
pygame.display.set_caption("生態系シミュ")
screen = pygame.display.set_mode((1280, 960))
tuti = (90, 60, 30)  # 土の色(茶色)
actors = []
for _ in range(30):
    actors.append(
        Nourishment(random.randint(20, 1260), random.randint(20, 940))
    )  # 養分を30個
for _ in range(10):
    actors.append(Plant(random.randint(20, 1260), random.randint(20, 940)))  # 植物を10個

while True:
    screen.fill(tuti)  # 背景を塗る

    for v in actors:
        v.step(screen, actors)  # アクターの処理
    removelist = filter(lambda x: not x.alive, actors)
    for v in removelist:
        if isinstance(v, BaseLife):
            for _ in range(v.energy):
                actors.append(
                    Nourishment(
                        v.x + random.randrange(-30, 30), v.y + random.randrange(-30, 30)
                    )
                )  # 生物除去時エネルギーを落とす
        actors.remove(v)
    pygame.display.update()  # 描画更新
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
    clock.tick(30)  # 30FPS

そしてこちらがmainです。といっても特筆するようなことは行っておらず、毎フレームactorたちのstepを呼び、
そのあと死んでいるactorを除去、それと同時に持っている養分を地面にぶちまけさせています。
これで世界の養分の総数を変えることなく植物のライフサイクルが作れます。

↑目次に戻る

🔗 あとがき

どうでしたか?植物が生えて、枯れて、また生える。
こんな単純なプログラムですが、枯れたときに養分が少し動くことで遠くまで植物が伝播したり、
逆に分断されてしまったり、そんな自然な感じをうまく表現できたと思います。
それでは、またいつか動物編でお会いしましょう。

↑目次に戻る


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


CONTACT

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