Pythonでインスタンスを理解する試み


![噴水２.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411052/e400a75b-4ba5-66ac-bb2a-30ae63654cf5.gif)




Pythonのクラスとインスタンスの力を使って、私たちは雨を降らせ、噴水を作り出すことができるのです。さあ、この驚くべき変身を目の当たりにしましょう！



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

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

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

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

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

``` py
import pygame
from pygame.locals import *
import sys

class Tubu:#１粒の水滴について記述したクラス
    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()

```

## １つの粒が揺れながら落下

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

``` 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()


```

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

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

クラスを使うメリットの一つに、オブジェクトを大量生産できるというものがありますが、まさにそれをやっています。
``` 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)           #ここで毎フレームごとに粒を１つ追加
        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()

```


## さびしいので噴水を３本に！
噴水自体を１つのクラス（Fountain）として、これを３本作ることで広場の噴水らしく見えるようにしました。
クラス名を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():                #噴水自体を１つのクラスとした
    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():#噴水１基分
    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():#噴水１基分
    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()

```