⚡ 게임 내에 등장하는 모든 객체들을 정의하는 부분이다
⚡ 객체들은 어느 정도는 같은 구조(struct)를 갖고있다
⚡ 반드시 구조를 따라야 되는건 아니다
객체의 움직임(update)과 관련된 코드들이 있다보니 어느면에서는 물리적 계산관 관련이 있지만 쉽게 이해할 수 있는정도이다. (코드를 다시보는데 이해가 안된다... 😅)
그럼 코드를 살펴보겠다
사실 오랜만에 보니 이해가 잘 안되는 요소들이 있어 설명이 부족할 수 있는데 양해 바란다... 😟
import os
import pygame as pg
import random
from time import sleep
from settings import *
vec = pg.math.Vector2
vec = pg.math.Vector2
: pygame 라이브러리의 Vector2 객체를 전역변수로 정의. 이후 방향, 속도가 필요한 변수에 사용된다
class Player(pg.sprite.Sprite):
def __init__(self, game):
self.groups = game.all_sprites
pg.sprite.Sprite.__init__(self, self.groups)
self.game = game
Sprite 객체 생성시 공통적으로 사용되는 부분이다
Start.py
에 있는 변수를 사용하기 위함이다✨ 여기서 Player 는 단일 객체기때문에 self.group
에 game.all_spirtes 밖에 없지만 Multi(group) 객체의 경우 다음과 같다.
class Platfomrs(pg.sprite.Spirte):
def __init__(self, game):
self.groups = game.all_sprites, game.platfomrs
pg.sprite.Sprite.__init__(self, self.groups)
...
✨ game
객체에서 game.platforms 를 그룹으로 묶은다음에, 상속받은 Sprite 초기화에 parameter로 넘긴다
여기서 game.platforms 은 Start.py
의 new 부분에서 정의한다
#Start..py
class Game(Ojbect):
def __init__(self):
....
def new(self):
...
# Group 객체
self.platforms = pg.sprite.Group()
self.bullets = pg.spirtes.Group()
# 단일 객체
self.player = Player(self)
...
이렇게 new 에서 각 group을 만들어주고 이후 필요시마다. all_spirte
과 group
에 add 하여 사용한다.
Ex
# 총알을 발사하는 event
#player 객체의 위치정보를 받아 bullet 객체 생성
bullet = Bullet(self, self.player.rect.centerx, self.player.rect.top)
self.all_sprites.add(bullet)
self.bullets.add(bullet)
...
쉽게 상황으로 설명을 하면
1. 발사키를 눌르면 event가 발생
2. event 가 발생하면 총알 발사(객체 생성)
3. 생성된 개체는 게임 안의 sprite group 에 add
4. bullet group 안에도 들어감
5. 이후 총알이 적과 충돌하면
6. 충돌한 총알 객체를 group에서 제거
총알을 예로들어 구현방법을 설명했는데, 다른 spirte 객체들도 이와 비슷한 방식으로 구현하면 된다
#시트에 동작별 프레임 단위로 나눠진 이미지 값들을 불러오는 함수
def load_images(self):
self.standing_frames = [self.game.stand.get_image(2, 0, 28, 40),
self.game.stand.get_image(32, 0, 29, 40),
self.game.stand.get_image(62, 0, 28, 40)]
for frame in self.standing_frames:
frame.set_colorkey(WHITE)
self.walk_frame_l = [self.game.move.get_image(0, 0, 30, 40),
self.game.move.get_image(30, 0, 30, 40)]
self.walk_frame_r = []
for frame in self.walk_frame_l:
frame.set_colorkey(WHITE)
for frame in self.walk_frame_l:
self.walk_frame_r.append(pg.transform.flip(frame, True, False))
self.jump_frame = self.game.jump.get_image(0, 0, 30, 40)
load_image
는 사용될 이미지를 갖고와서 프레임단위로 저장한다 #이미지 프레임별 애니메이션이 적용되는 함수
def animate(self):
now = pg.time.get_ticks() #millisecond 단위로 현재 게임시간을 갖고옴
#움직임 값(Bollean)에 대한 명세
if self.vel.x != 0:
self.walking = True
else:
self.walking = False
#player가 움직이고 있을 떄(True) 애니메이션
if self.walking:
if now - self.last_update > 120: #프레임 속도를 위한 값
self.last_update = now
#현재 프레임 값은 계속 1씩 증가하면서 이미지의 프레임 길이만큼 나눠짐
#즉, 프레임 길이에 따라서 current_frame값이 바뀜
self.current_frame = (self.current_frame + 1) % len(self.walk_frame_l)
if self.vel.x > 0: #바뀌는 current_frame 값이 이미지의 프레임을 순회.
self.image = self.walk_frame_r[self.current_frame]
else:
self.image = self.walk_frame_l[self.current_frame]
if self.vel.y != 0:
self.jumping = True
#제자리 점프에 대한 애니메이션
if self.jumping and not self.walking:
if now - self.last_update > 150:
self.last_update = now
self.current_frame = (self.current_frame + 1) % 1
self.image = self.jump_frame
self.image.set_colorkey(GREEN)
self.mask = pg.mask.from_surface(self.image) #플레이어 mask
#가만히 있을때 애니메이션
if not self.jumping and not self.walking:
if now - self.last_update > 350:
self.last_update = now
self.current_frame = (self.current_frame + 1) % len(self.standing_frames)
self.image = self.standing_frames[self.current_frame]
animate
메서드는 프레임 단위로 동작들에 대한 애니메이션(이미지)에 대한 정의 이다. 즉, 가만히 있거나, 점프 중이거나, 이동중이나 등.. 어느 한 행동에 대한 프레임단위별 애니메이션을 그려주는 역활을 한다.
def update(self):
self.animate()
self.acc = vec(0, PLAYER_GRAV)#가속값 -> X는 0이고 Y는 중력이 적용
keys = pg.key.get_pressed()
if keys[pg.K_LEFT]:
self.acc.x = -PLAYER_ACC
if keys[pg.K_RIGHT]:
self.acc.x = PLAYER_ACC
# apply friction
self.acc.x += self.vel.x * PLAYER_FRICTION #X에만 마찰력이 적용됨 (Y는 중력 적용)
#equations of motion
self.vel += self.acc
if abs(self.vel.x) < 0.1:
self.vel.x = 0
self.pos += self.vel + 0.5 * self.acc
#wrap around the sides of the screen
if self.pos.x > WIDTH + self.rect.width / 2:
self.pos.x = 0 - self.rect.width / 2
if self.pos.x < 0 - self.rect.width / 2:
self.pos.x = WIDTH + self.rect.width / 2
self.rect.midbottom = self.pos
update
는 어떻게보면 sprite에서 제일 중요한 역활을 한다. 이전에 게임의 루프는 event->update->draw 라고 설명한적이 있다.
update
메서드는 바로 update->draw 를 처리하는 역활을한다. 그래서 주로 매 프레임마다 event를 체크하고 event에 대한 값과, 애니메이션을 update 한다.
여기 까지가 일반적인 sprite의 구현이다. 다른 sprite라고 해서 큰 틀을 벗어나지 않으며 최대한 같은 구조를 유지하여 개발하는게 이후 디버깅에서 편리하다.
💥 참고로 소스코드로 설명하는 메소드들의 이름(name)은 주관적이며 다르게해도 상관없다
이제 이전에 설명안하고 남겼던 run 코드를 보자.
def run(self):
#game loop
self.playing = True
while self.playing:
self.clock.tick(FPS)
self.events()
self.update()
self.draw()
이제 어느정도 이해가 되는가? 😅
결국 게임의 흐름은 loop(while)를 돌면서
1. 키 이벤트를 체크하고 ( event )
2. 매 프레임마다 변경 사항들을 반영하고 ( update )
3. 이를 화면에 그려준다 ( draw )
run 은 게임의 전체를 의미한다. 즉, event, update, draw 는 게임의 전체에 대한 loop이다. 이 말은 무엇이냐면.
event 는 게임 전체에 대한 이벤트를 감지( ex) 키 이벤트)
update 또한 게임 전체에 대한 변경 사항들을 반영
update 안에서 모든 sprite 들의 update&draw 를 하나로 묶어 update 하고, 게임 전체에 대한 update를 수행(점수 변경, 객체 간의 충돌...)
draw 는 update 에서 변경된 sprite의 위치에 맞게 다시 그려준다
사실 설명하면서도 내가 무슨 말을 하는건지 이해가 안되는부분이 많았는데... 너무 어렵고 복잡하다고 좌절하지말자.
본인도 유튜브와 래퍼런스 문서를 계속 보고 따라하면서 게임을 완성할 수 있었다.
pygame으로 게임 개발에 도전하고 싶다면 한번쯤은 해볼만한 프로젝트라고 생각한다. (기왕이면 더 쉬운 tool을 쓰자.. )
이상으로 허접한 설명을 마치겠다 더 많은 정보를 원한다면 아래의 링크들을 참고바란다. 👍