LoginSignup
1
0

デジタル水滴の舞:Pythonで噴水を描く

Last updated at Posted at 2024-01-29

噴水2.gif

Pygameの魔法の世界へようこそ!ここでは、単なるコードが生命を吹き込まれ、驚異的なことを成し遂げます。今日のショーでは、なんと一粒の水滴が主役です。そう、たった一つの水滴クラスを使って、まるで魔法のように噴水を生み出します。このデモでは、わずか一滴の水が、クラスの魔法の手にかかると、まるで雨あられのように降り注ぎ、噴水と化します。言い換えれば、一粒の水滴から始まり、無数のインスタンスが合唱するようにして、壮大な噴水の光景を創り出します。
Pythonのクラスとインスタンスの力を使って、私たちは雨を降らせ、噴水を作り出すことができるのです。さあ、この驚くべき変身を目の当たりにしましょう!

とまあ大げさな書き出しですが(chatGPTで書きました;;;)、インスタンスを大量生産するとこんな事もできます。
https://www.youtube.com/watch?v=ahf0DUzdZjk

ちょっとずつ進めていきますので、VSコードエディタを使っている人はcompare selectedでコードを比較しながら見ていただけると幸いです。

前提:グラフィックライブラリとしてpygameを使います。
pygame.draw.circle()をつかって小さい丸を描きこれを粒とします。

いつものpygameのテンプレートを使っていきます。
https://qiita.com/bkh4149/items/25169ffbd375dcc19740
このページの一番上にあるコードです)

まずは粒(円)を書く部分をクラス化していきます。

import pygame
from pygame.locals import *
import sys

class Tubu:#1粒の水滴について記述したクラス
    def __init__(self):
        self.px=120
        self.py=100
    def update(self):#落下
        self.py=self.py+0.1
    def draw(self,screen):    
        pygame.draw.circle(screen,(10,10,10),(self.px,self.py),50)              

def main():
    pygame.init()                                 # Pygameの初期化
    screen = pygame.display.set_mode((800, 600))  # 800*600の画面
    T1=Tubu()              #◆ここでインスタンス化
    while True:
        screen.fill((255,255,255))              # 背景を白
        T1.update()        #◆ここでメソッド使用
        T1.draw(screen)    #◆ここでメソッド使用

        pygame.display.update()               # 画面更新

        # イベント処理
        for event in pygame.event.get():  # イベントを取得
            if event.type == QUIT:        # 閉じるボタンが押されたら
                pygame.quit()             
                sys.exit()                # 終了
if __name__ == "__main__":
    main()

1つの粒が揺れながら落下

pygame.draw.circle()で描いたマルを粒と見立て、揺れながら落ちるようにしたのがg0.pyです
x方向もランダムで動かすことで1つの粒が揺れながらおちていきます。  

g0.py

import pygame
from pygame.locals import *
import sys
import random

class Tubu():
    def __init__(self,x,y):
        self.x=x
        self.y=y
    def update(self):
        self.y+=0.5
        self.x+=random.randint(-2,2)  #◆x方向もランダムで動かす
    def draw(self,screen):
        pygame.draw.circle(screen,(210,10,10),(self.x,self.y),10) 

def main():
    pygame.init()                                 
    screen = pygame.display.set_mode((800, 600))  
    T1=Tubu(400,100)
    while True:
        screen.fill((255,255,255))   
        T1.update()
        T1.draw(screen)
        pygame.display.update()  

        # イベント処理
        for event in pygame.event.get():  
            if event.type == QUIT:        
                pygame.quit()             
                sys.exit()                
if __name__ == "__main__":
    main()


クラスで粒をたくさん描画

ここまで粒は1つだけでしたが、いよいよクラスの大量生産機能を使ってどんどんインスタンス化していきます
毎フレームごとに粒を1つ追加していくのであっという間に大量の粒ができます。

クラスを使うメリットの一つに、オブジェクトを大量生産できるというものがありますが、まさにそれをやっています。

g1.py

import pygame
from pygame.locals import *
import sys
import random

class Tubu():
    def __init__(self,x,y):
        self.x=x
        self.y=y
    def update(self):
        self.y+=0.5
        self.x+=random.randint(-1,1)
    def draw(self,screen):
        pygame.draw.circle(screen,(210,10,10),(self.x,self.y),3) 

def main():
    pygame.init()                                 
    screen = pygame.display.set_mode((800, 600))  
    Ts=[]                        #大量の粒を入れる容れ物、最初は空
    while True:
        screen.fill((255,255,255))                            
        T1=Tubu(400,100)
        Ts.append(T1)           #ここで毎フレームごとに粒を1つ追加
        for T in Ts:
            T.update()
            T.draw(screen)
        pygame.display.update()                               

        # イベント処理
        for event in pygame.event.get(): 
            if event.type == QUIT:       
                pygame.quit()             
                sys.exit()               
if __name__ == "__main__":
    main()


丸を大きくすると血がどくどく!

ちなみにマルのサイズを大きくしたら、色が赤だったこともあってどくどくと血が流れるようなデモになってしまいました。まあこれはこれでスプラッタ系のゲームを作るときに使えそうですね

g2.py

import pygame
from pygame.locals import *
import sys
import random

class Tubu():
    def __init__(self,x,y):
        self.x=x
        self.y=y
    def update(self):
        self.y+=0.5
        self.x+=random.randint(-1,1)
    def draw(self,screen):
        pygame.draw.circle(screen,(210,10,10),(self.x,self.y),20) 

def main():
    pygame.init()                                 
    screen = pygame.display.set_mode((800, 600))  
    Ts=[]
    while True:
        screen.fill((255,255,255))                
        T1=Tubu(400,100)
        Ts.append(T1)
        for T in Ts:
            T.update()
            T.draw(screen)
        pygame.display.update()                   

        # イベント処理
        for event in pygame.event.get(): 
            if event.type == QUIT:       
                pygame.quit()             
                sys.exit()               
if __name__ == "__main__":
    main()

粒に重力の要素を加える

粒の色を青系にして、下から上に上げてから落下させると噴水に見えます。

g3.py

import pygame
from pygame.locals import *
import sys
import random

class Tubu():
    def __init__(self,x,y):
        self.x=x
        self.y=y
        self.ay=-1
        self.a=0.01
    def update(self):
        self.ay+=self.a
        self.y+=self.ay
        self.x+=random.randint(-2,2)
    def draw(self,screen):
        pygame.draw.circle(screen,(10,10,210),(self.x,self.y),2) 

def main():
    pygame.init()                        
    screen = pygame.display.set_mode((800, 600))
    Ts=[]
    while True:
        screen.fill((255,255,255))       
        T1=Tubu(400,300)
        Ts.append(T1)
        for T in Ts:
            T.update()
            T.draw(screen)
        pygame.display.update()                         
        # イベント処理
        for event in pygame.event.get():
        if event.type == QUIT:
                pygame.quit()             
                sys.exit()    
if __name__ == "__main__":
    main()

噴水らしく

噴水っぽく見えるように、粒を高く吹き上げて全体を細長い形にしました。

g4.py

import pygame
from pygame.locals import *
import sys
import random

class Tubu():
    def __init__(self,x,y):
        self.x=x
        self.y=y
        self.vy=-2
        self.a=0.01
    def update(self):
        self.vy+=self.a
        self.y+=self.vy
        if self.vy>0:
            self.x+=random.randint(-3,3)
        else :   
            self.x+=random.randint(-1,1)/3
    def draw(self,screen):
        pygame.draw.circle(screen,(10,10,210),(self.x,self.y),2) 

def main():
    pygame.init()             
    screen = pygame.display.set_mode((800, 600))
    Ts=[]
    while True:
        screen.fill((255,255,255))    
        T1=Tubu(400,400)
        Ts.append(T1)
        for T in Ts:
            T.update()
            T.draw(screen)
        pygame.display.update()       

        # イベント処理
        for event in pygame.event.get():
            if event.type == QUIT:  
                pygame.quit()             
                sys.exit()  
if __name__ == "__main__":
    main()

さびしいので噴水を3本に!

噴水自体を1つのクラス(Fountain)として、これを3本作ることで広場の噴水らしく見えるようにしました。
クラス名をTubuからDropに変更しました。

g5.py

import pygame
from pygame.locals import *
import sys
import random

class Drop():
    def __init__(self,x,y):
        self.x=x
        self.y=y
        self.vy=-2
        self.a=0.01
    def update(self):
        self.vy+=self.a
        self.y+=self.vy
        if self.vy>0:
            self.x+=random.randint(-3,3)
        else :   
            self.x+=random.randint(-1,1)/3
    def draw(self,screen):
        pygame.draw.circle(screen,(10,10,210),(self.x,self.y),2) 

class Fountain():                #噴水自体を1つのクラスとした
    def __init__(self,x,y):
        self.x=x
        self.y=y
        self.Gs=[]
    def update(self,screen):
        D1=Drop(self.x,self.y)
        self.Gs.append(D1)
        for T in self.Gs:
            T.update()
            T.draw(screen)
def main():
    pygame.init()                                 
    screen = pygame.display.set_mode((800, 600))  
    F1=Fountain(400,400)
    F2=Fountain(200,400)
    F3=Fountain(600,400)
    while True:
        screen.fill((255,255,255))                
        F1.update(screen)
        F2.update(screen)
        F3.update(screen)
        pygame.display.update()                   

        # イベント処理
        for event in pygame.event.get():  
            if event.type == QUIT:        
                pygame.quit()             
                sys.exit()                
if __name__ == "__main__":
    main()

やっぱり噴水を9本に!

それでもなんか寂しいので噴水を増やしてみました。
ここで気がついたのですが、時間が経つとだんだん遅くなっていきます。
さらに時間が経つとスローモーションみたいになってしまうのは、水滴が多すぎるのですね。なので画面外にはみ出した水滴は削除しないといけません。
削除だと意外と面倒な気がしたので逆にはみださなかったやつを再び回収するということにしました。
self.Ds = [D for D in self.Ds if D.y <= HEIGHT] # 画面外にはみ出さなかった水滴をself.Dsに入れる

g6.py

import pygame
from pygame.locals import *
import sys
import random
WATER=(10,100,255)
HEIGHT=600
WIDTH=800

class Drop():
    def __init__(self,x,y):
        self.x=x
        self.y=y
        self.vy=-2
        self.a=0.01
    def update(self):
        self.vy+=self.a
        self.y+=self.vy
        if self.vy>0:
            self.x+=random.randint(-2,2)
        else :   
            self.x+=random.randint(-1,1)/3
    def draw(self,screen):
        pygame.draw.circle(screen,WATER,(self.x,self.y),2) 

class Fountain():
    def __init__(self,x,y):
        self.x=x
        self.y=y
        self.Ds=[]
    def update(self,screen):
        # 新しいDropインスタンスを追加
        self.Ds.append(Drop(self.x, self.y))
        # Dropインスタンスの更新と描画、画面内にあるインスタンスのみを保持
        self.Ds = [D for D in self.Ds if D.y <= HEIGHT]  # 画面の高さが600の場合        
        for D in self.Ds:
            D.update()
            D.draw(screen)


def main():
    pygame.init()                                 
    screen = pygame.display.set_mode((WIDTH, HEIGHT)) 
    ds=[(400,400),(200,400),(600,400),(300,540),
    (500,540),(100,240),(300,240),(500,240),(700,240),]

    Fs=[Fountain(d[0],d[1]) for d in ds]

    while True:
        screen.fill((255,255,255))                    
        for F in Fs:
          F.update(screen)
        pygame.display.update()     

        # イベント処理
        for event in pygame.event.get():
            if event.type == QUIT:      
                pygame.quit()             
                sys.exit()              
if __name__ == "__main__":
    main()


吹き上がる高さを変更!

より噴水らしく見えるように一定時間経過したら噴水の高さが上下するように変更しました。

g7.py
import pygame
from pygame.locals import *
import sys
import random
WATER=(10,100,255)
HEIGHT=600
WIDTH=800

class Drop():
    def __init__(self,x,y,vy):
        self.x=x
        self.y=y
        self.vy=vy
        self.a=0.01
    def update(self):
        self.vy+=self.a
        self.y+=self.vy
        if self.vy>0:
            self.x+=random.randint(-2,2)
        else :   
            self.x+=random.randint(-1,1)/3
    def draw(self,screen):
        pygame.draw.circle(screen,WATER,(self.x,self.y),2) 

class Fountain():
    def __init__(self,x,y):
        self.x=x
        self.y=y
        self.Ds=[]
        self.ct=0
    def update(self,screen):
        self.ct+=1
        # 新しいDropインスタンスを追加
        if self.ct%1000<500:
            self.Ds.append(Drop(self.x, self.y, -1.3))
        else:
            self.Ds.append(Drop(self.x, self.y, -2))
                
        # Dropインスタンスの更新と描画、画面内にあるインスタンスのみを保持
        self.Ds = [D for D in self.Ds if D.y <= HEIGHT]          
        for D in self.Ds:
            D.update()
            D.draw(screen)


def main():
    pygame.init()                                 # Pygameの初期化
    screen = pygame.display.set_mode((WIDTH, HEIGHT))  
    bases=[(400,400),(200,400),(600,400),(300,540),
    (500,540),(100,240),(300,240),(500,240),(700,240),]

    Fs=[Fountain(d[0],d[1]) for d in bases]
    while True:
        screen.fill((255,255,255))                                    # 背景を白
        for F in Fs:
          F.update(screen)
        pygame.display.update()                                       # 画面更新

        # イベント処理
        for event in pygame.event.get():  # イベントを取得
            if event.type == QUIT:        # 閉じるボタンが押されたら
                pygame.quit()             
                sys.exit()                # 終了
if __name__ == "__main__":
    main()

円のように並べ、噴水ショーっぽく演出

ドバイの噴水ショーをみていたら感動したので、それっぽく噴水を楕円状にならべてみました。

g8.py

import pygame
from pygame.locals import *
import sys
import random
import math

#全体のパラメータ
WATER1=(100,100,255)#色
HEIGHT=600#画面サイズ
WIDTH=800

class Tubu():
    def __init__(self,x,y,vy):
        self.x=x
        self.y=y
        self.vy=vy
        self.a=0.03
    def update(self):
        self.vy+=self.a
        self.y+=self.vy
        if self.vy>0:
            self.x+=random.randint(-2,2)
        else :   
            self.x+=random.randint(-1,1)/3
    def draw(self,screen):
        pygame.draw.circle(screen,WATER1,(self.x,self.y),2) 
        

class Fountain():#噴水1基分
    def __init__(self,x,y,ct):
        self.x=x
        self.y=y
        self.Ds=[]
        self.id=ct
        self.ct=ct*100
        #print(self.ct)
    def update(self,screen):
        self.ct+=1
        #print(f"{self.id=},{self.ct=}")
        # 新しいTubuインスタンスを追加
        if self.ct%3000<300:
            self.Ds.append(Tubu(self.x, self.y, -2))
        else:
            self.Ds.append(Tubu(self.x, self.y, -3))
        # Tubuインスタンスの更新と描画、画面内にあるインスタンスのみを保持
        self.Ds = [D for D in self.Ds if D.y <= HEIGHT]          
        for D in self.Ds:
            D.update()
            D.draw(screen)

def main():
    pygame.init()              # Pygameの初期化
    screen = pygame.display.set_mode((WIDTH, HEIGHT))  

    # 噴水を楕円状に配置するためのベースの位置を計算
    bases=[]
    # 楕円用のパラメータ
    center_x, center_y = WIDTH // 2, HEIGHT // 2
    a, b = 300, 200  # 水平軸と垂直軸の長さ    
    dv=20    # 20個の点を楕円状に描画
    for i in range(dv):
        angle = 2 * math.pi * i / dv
        x = center_x + a * math.cos(angle)
        y = center_y + b * math.sin(angle)
        bases.append([int(x),int(y),i])

    Fs=[Fountain(d[0],d[1],d[2]) for d in bases]
    while True:
        screen.fill((255,255,255))       # 背景を白
        for F in Fs:
          F.update(screen)
        pygame.display.update()          # 画面更新

        # イベント処理
        for event in pygame.event.get():  # イベントを取得
            if event.type == QUIT:        # 閉じるボタンが押されたら
                pygame.quit()             
                sys.exit()                # 終了
if __name__ == "__main__":
    main()

演目を変えられるようにした

関数を引数として使用することによって柔軟にクラスのメソッドの動き方を変えることができます。

g9.py

import pygame
from pygame.locals import *
import sys
import random
import math


WATER1=(100,100,255)
WATER2=(200,200,255)
WATER3=(10,200,255)
HEIGHT=600
WIDTH=800
# 楕円のパラメータ
center_x, center_y = WIDTH // 2, HEIGHT // 2
a, b = 300, 200  # 水平軸と垂直軸の長さ

class Drop():
    def __init__(self,x,y,vy):
        self.x=x
        self.y=y
        self.vy=vy
        self.a=0.03
    def update(self):
        self.vy+=self.a
        self.y+=self.vy
        if self.vy>0:
            self.x+=random.randint(-2,2)
        else :   
            self.x+=random.randint(-1,1)/3
    def draw(self,screen):
        pygame.draw.circle(screen,WATER1,(self.x,self.y),2) 
        

class Fountain():#噴水1基分
    def __init__(self,x,y,ct):
        self.x=x
        self.y=y
        self.Ds=[]
        self.id=ct
        self.ct=ct*100
        #print(self.ct)
        def update(self,screen,f):       #関数を引数としてうけとる
        self.ct+=1
        #print(f"{self.id=},{self.ct=}")
        # 新しいDropインスタンスを追加
        f()
        # Dropインスタンスの更新と描画、画面内にあるインスタンスのみを保持
        self.Ds = [D for D in self.Ds if D.y <= HEIGHT]          
        for D in self.Ds:
            D.update()
            D.draw(screen)
            
    #関数を引数として使用することによって柔軟にクラスのメソッドの動き方を変えることができる
    def f1(self):
        if self.ct%3000<300:
            self.Ds.append(Drop(self.x, self.y, -2))
        else:
            self.Ds.append(Drop(self.x, self.y, -3))
    def f2(self):
        if self.ct%1000<300:
            self.Ds.append(Drop(self.x, self.y, -1))
        else:
            self.Ds.append(Drop(self.x, self.y, -4))

def main():
    pygame.init()                        
    screen = pygame.display.set_mode((WIDTH, HEIGHT))  

    # 20個の点を楕円状に描画
    bases=[]#噴水の基点座標
    dv=30
    for i in range(dv):
        angle = 2 * math.pi * i / dv
        x = center_x + a * math.cos(angle)
        y = center_y + b * math.sin(angle)
        pygame.draw.circle(screen, (255, 255, 255), (int(x), int(y)), 5)
        bases.append([int(x),int(y),i])
    #print(bases)   
    Fs=[Fountain(d[0],d[1],d[2]) for d in bases]
    mainCt=0
    while True:
        mainCt+=1
        screen.fill((255,255,255))       
        for F in Fs:
          if mainCt<1000:
              F.update(screen,F.f1)
          else:    
              F.update(screen,F.f2)
        pygame.display.update()          
        # イベント処理
        for event in pygame.event.get(): 
            if event.type == QUIT:       
                pygame.quit()             
                sys.exit()               
if __name__ == "__main__":
    main()

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0