dev-jakklu #4

Merged
s452701 merged 4 commits from dev-jakklu into master 2021-03-28 18:57:21 +02:00
24 changed files with 663 additions and 65 deletions

View File

@ -1,20 +1,22 @@
import pygame
from settings import SCREEN_WIDTH, SCREEN_HEIGHT
from survival import esper
from survival.camera import Camera
from survival.components.camera_target_component import CameraTargetComponent
from survival.components.input_component import InputComponent
from survival.components.movement_component import MovementComponent
from survival.components.position_component import PositionComponent
from survival.components.sprite_component import SpriteComponent
from survival.game_map import GameMap
def draw_game(delta):
win.fill((0, 0, 0))
game_map.draw(camera)
pygame.display.update()
def update_game(delta, pressed_keys):
game_map.update(camera, delta, pressed_keys)
pass
from survival.player_generator import PlayerGenerator
from survival.resource_generator import ResourceGenerator
from survival.systems.camera_system import CameraSystem
from survival.systems.collision_system import CollisionSystem
from survival.systems.draw_system import DrawSystem
from survival.systems.input_system import InputSystem
from survival.systems.movement_system import MovementSystem
from survival.world_generator import WorldGenerator
if __name__ == '__main__':
pygame.init()
@ -26,6 +28,12 @@ if __name__ == '__main__':
game_map = GameMap(int(SCREEN_WIDTH / 32) * 2, 2 * int(SCREEN_HEIGHT / 32) + 1)
camera = Camera(game_map.width * 32, game_map.height * 32, win)
world = WorldGenerator().create_world(camera, game_map)
player = PlayerGenerator().create_player(world, game_map)
ResourceGenerator(world, game_map).generate_resources()
run = True
while run:
@ -40,5 +48,7 @@ if __name__ == '__main__':
keys = pygame.key.get_pressed()
draw_game(ms)
update_game(ms, keys)
win.fill((0, 0, 0))
game_map.draw(camera)
world.process(ms)
pygame.display.update()

View File

@ -21,8 +21,8 @@ class Camera:
SCREEN_WIDTH - self.camera.left, SCREEN_HEIGHT - self.camera.top)
def update(self, target):
x = -target.pos[0] + int(SCREEN_WIDTH / 2)
y = -target.pos[1] + int(SCREEN_HEIGHT / 2)
x = -target.position[0] + int(SCREEN_WIDTH / 2)
y = -target.position[1] + int(SCREEN_HEIGHT / 2)
x = min(0, x)
y = min(0, y)

View File

@ -0,0 +1,3 @@
class CameraTargetComponent:
def __init__(self, target):
self.target = target

View File

@ -0,0 +1,3 @@
class CollisionComponent:
def __init__(self):
pass

View File

@ -0,0 +1,3 @@
class InputComponent:
def __init__(self):
self.input_data = None

View File

@ -0,0 +1,4 @@
class MovementComponent:
def __init__(self):
self.speed = 30
self.timer = 0

View File

@ -0,0 +1,5 @@
class MovingComponent:
def __init__(self, direction, target):
self.direction = direction
self.movement_target = target
self.checked_collision = False

View File

@ -0,0 +1,4 @@
class PositionComponent:
def __init__(self, pos, grid_pos):
self.position = pos
self.grid_position = grid_pos

View File

@ -0,0 +1,9 @@
from survival.image import Image
class SpriteComponent:
def __init__(self, path):
self.image = Image(path)
def set_scale(self, scale):
self.image.set_scale(scale)

22
survival/entity_layer.py Normal file
View File

@ -0,0 +1,22 @@
class EntityLayer:
def __init__(self, width, height):
self.width = width
self.height = height
self.tiles = [[None for x in range(self.width)] for y in range(self.height)]
def draw(self, camera, visible_area):
pass
def add_entity(self, entity, pos):
self.tiles[pos[1]][pos[0]] = entity
def move_entity(self, from_pos, to_pos):
ent = self.tiles[from_pos[1]][from_pos[0]]
self.tiles[from_pos[1]][from_pos[0]] = None
self.tiles[to_pos[1]][to_pos[0]] = ent
def remove_entity(self, pos):
self.tiles[pos[1]][pos[0]] = None
def is_colliding(self, pos):
return self.tiles[pos[1]][pos[0]] is not None

368
survival/esper.py Normal file
View File

@ -0,0 +1,368 @@
import time as _time
from functools import lru_cache as _lru_cache
from typing import List as _List
from typing import Type as _Type
from typing import TypeVar as _TypeVar
from typing import Any as _Any
from typing import Tuple as _Tuple
from typing import Iterable as _Iterable
version = '1.3'
C = _TypeVar('C')
P = _TypeVar('P')
class Processor:
"""Base class for all Processors to inherit from.
Processor instances must contain a `process` method. Other than that,
you are free to add any additional methods that are necessary. The process
method will be called by each call to `World.process`, so you will
generally want to iterate over entities with one (or more) calls to the
appropriate world methods there, such as
`for ent, (rend, vel) in self.world.get_components(Renderable, Velocity):`
"""
world = None
def process(self, *args, **kwargs):
raise NotImplementedError
class World:
"""A World object keeps track of all Entities, Components, and Processors.
A World contains a database of all Entity/Component assignments. The World
is also responsible for executing all Processors assigned to it for each
frame of your game.
"""
def __init__(self, timed=False):
self._processors = []
self._next_entity_id = 0
self._components = {}
self._entities = {}
self._dead_entities = set()
if timed:
self.process_times = {}
self._process = self._timed_process
def clear_cache(self) -> None:
self.get_component.cache_clear()
self.get_components.cache_clear()
def clear_database(self) -> None:
"""Remove all Entities and Components from the World."""
self._next_entity_id = 0
self._dead_entities.clear()
self._components.clear()
self._entities.clear()
self.clear_cache()
def add_processor(self, processor_instance: Processor, priority=0) -> None:
"""Add a Processor instance to the World.
:param processor_instance: An instance of a Processor,
subclassed from the Processor class
:param priority: A higher number is processed first.
"""
assert issubclass(processor_instance.__class__, Processor)
processor_instance.priority = priority
processor_instance.world = self
self._processors.append(processor_instance)
self._processors.sort(key=lambda proc: proc.priority, reverse=True)
def remove_processor(self, processor_type: Processor) -> None:
"""Remove a Processor from the World, by type.
:param processor_type: The class type of the Processor to remove.
"""
for processor in self._processors:
if type(processor) == processor_type:
processor.world = None
self._processors.remove(processor)
def get_processor(self, processor_type: _Type[P]) -> P:
"""Get a Processor instance, by type.
This method returns a Processor instance by type. This could be
useful in certain situations, such as wanting to call a method on a
Processor, from within another Processor.
:param processor_type: The type of the Processor you wish to retrieve.
:return: A Processor instance that has previously been added to the World.
"""
for processor in self._processors:
if type(processor) == processor_type:
return processor
def create_entity(self, *components) -> int:
"""Create a new Entity.
This method returns an Entity ID, which is just a plain integer.
You can optionally pass one or more Component instances to be
assigned to the Entity.
:param components: Optional components to be assigned to the
entity on creation.
:return: The next Entity ID in sequence.
"""
self._next_entity_id += 1
# TODO: duplicate add_component code here for performance
for component in components:
self.add_component(self._next_entity_id, component)
# self.clear_cache()
return self._next_entity_id
def delete_entity(self, entity: int, immediate=False) -> None:
"""Delete an Entity from the World.
Delete an Entity and all of it's assigned Component instances from
the world. By default, Entity deletion is delayed until the next call
to *World.process*. You can request immediate deletion, however, by
passing the "immediate=True" parameter. This should generally not be
done during Entity iteration (calls to World.get_component/s).
Raises a KeyError if the given entity does not exist in the database.
:param entity: The Entity ID you wish to delete.
:param immediate: If True, delete the Entity immediately.
"""
if immediate:
for component_type in self._entities[entity]:
self._components[component_type].discard(entity)
if not self._components[component_type]:
del self._components[component_type]
del self._entities[entity]
self.clear_cache()
else:
self._dead_entities.add(entity)
def entity_exists(self, entity: int) -> bool:
"""Check if a specific entity exists.
Empty entities(with no components) and dead entities(destroyed
by delete_entity) will not count as existent ones.
:param entity: The Entity ID to check existance for.
:return: True if the entity exists, False otherwise.
"""
return entity in self._entities and entity not in self._dead_entities
def component_for_entity(self, entity: int, component_type: _Type[C]) -> C:
"""Retrieve a Component instance for a specific Entity.
Retrieve a Component instance for a specific Entity. In some cases,
it may be necessary to access a specific Component instance.
For example: directly modifying a Component to handle user input.
Raises a KeyError if the given Entity and Component do not exist.
:param entity: The Entity ID to retrieve the Component for.
:param component_type: The Component instance you wish to retrieve.
:return: The Component instance requested for the given Entity ID.
"""
return self._entities[entity][component_type]
def components_for_entity(self, entity: int) -> _Tuple[C, ...]:
"""Retrieve all Components for a specific Entity, as a Tuple.
Retrieve all Components for a specific Entity. The method is probably
not appropriate to use in your Processors, but might be useful for
saving state, or passing specific Components between World instances.
Unlike most other methods, this returns all of the Components as a
Tuple in one batch, instead of returning a Generator for iteration.
Raises a KeyError if the given entity does not exist in the database.
:param entity: The Entity ID to retrieve the Components for.
:return: A tuple of all Component instances that have been
assigned to the passed Entity ID.
"""
return tuple(self._entities[entity].values())
def has_component(self, entity: int, component_type: _Any) -> bool:
"""Check if a specific Entity has a Component of a certain type.
:param entity: The Entity you are querying.
:param component_type: The type of Component to check for.
:return: True if the Entity has a Component of this type,
otherwise False
"""
return component_type in self._entities[entity]
def has_components(self, entity: int, *component_types: _Any) -> bool:
"""Check if an Entity has all of the specified Component types.
:param entity: The Entity you are querying.
:param component_types: Two or more Component types to check for.
:return: True if the Entity has all of the Components,
otherwise False
"""
return all(comp_type in self._entities[entity] for comp_type in component_types)
def add_component(self, entity: int, component_instance: _Any, type_alias: _Type = None) -> None:
"""Add a new Component instance to an Entity.
Add a Component instance to an Entiy. If a Component of the same type
is already assigned to the Entity, it will be replaced.
:param entity: The Entity to associate the Component with.
:param component_instance: A Component instance.
"""
component_type = type_alias or type(component_instance)
if component_type not in self._components:
self._components[component_type] = set()
self._components[component_type].add(entity)
if entity not in self._entities:
self._entities[entity] = {}
self._entities[entity][component_type] = component_instance
self.clear_cache()
def remove_component(self, entity: int, component_type: _Any) -> int:
"""Remove a Component instance from an Entity, by type.
A Component instance can be removed by providing it's type.
For example: world.delete_component(enemy_a, Velocity) will remove
the Velocity instance from the Entity enemy_a.
Raises a KeyError if either the given entity or Component type does
not exist in the database.
:param entity: The Entity to remove the Component from.
:param component_type: The type of the Component to remove.
"""
self._components[component_type].discard(entity)
if not self._components[component_type]:
del self._components[component_type]
del self._entities[entity][component_type]
if not self._entities[entity]:
del self._entities[entity]
self.clear_cache()
return entity
def _get_component(self, component_type: _Type[C]) -> _Iterable[_Tuple[int, C]]:
"""Get an iterator for Entity, Component pairs.
:param component_type: The Component type to retrieve.
:return: An iterator for (Entity, Component) tuples.
"""
entity_db = self._entities
for entity in self._components.get(component_type, []):
yield entity, entity_db[entity][component_type]
def _get_components(self, *component_types: _Type) -> _Iterable[_Tuple[int, ...]]:
"""Get an iterator for Entity and multiple Component sets.
:param component_types: Two or more Component types.
:return: An iterator for Entity, (Component1, Component2, etc)
tuples.
"""
entity_db = self._entities
comp_db = self._components
try:
for entity in set.intersection(*[comp_db[ct] for ct in component_types]):
yield entity, [entity_db[entity][ct] for ct in component_types]
except KeyError:
pass
@_lru_cache()
def get_component(self, component_type: _Type[C]) -> _List[_Tuple[int, C]]:
return [query for query in self._get_component(component_type)]
@_lru_cache()
def get_components(self, *component_types: _Type):
return [query for query in self._get_components(*component_types)]
def try_component(self, entity: int, component_type: _Type):
"""Try to get a single component type for an Entity.
This method will return the requested Component if it exists, but
will pass silently if it does not. This allows a way to access
optional Components that may or may not exist, without having to
first querty the Entity to see if it has the Component type.
:param entity: The Entity ID to retrieve the Component for.
:param component_type: The Component instance you wish to retrieve.
:return: A iterator containg the single Component instance requested,
which is empty if the component doesn't exist.
"""
if component_type in self._entities[entity]:
yield self._entities[entity][component_type]
else:
return None
def try_components(self, entity: int, *component_types: _Type):
"""Try to get a multiple component types for an Entity.
This method will return the requested Components if they exist, but
will pass silently if they do not. This allows a way to access
optional Components that may or may not exist, without first having
to query if the entity has the Component types.
:param entity: The Entity ID to retrieve the Component for.
:param component_types: The Components types you wish to retrieve.
:return: A iterator containg the multiple Component instances requested,
which is empty if the components do not exist.
"""
if all(comp_type in self._entities[entity] for comp_type in component_types):
yield [self._entities[entity][comp_type] for comp_type in component_types]
else:
return None
def _clear_dead_entities(self):
"""Finalize deletion of any Entities that are marked dead.
In the interest of performance, this method duplicates code from the
`delete_entity` method. If that method is changed, those changes should
be duplicated here as well.
"""
for entity in self._dead_entities:
for component_type in self._entities[entity]:
self._components[component_type].discard(entity)
if not self._components[component_type]:
del self._components[component_type]
del self._entities[entity]
self._dead_entities.clear()
self.clear_cache()
def _process(self, *args, **kwargs):
for processor in self._processors:
processor.process(*args, **kwargs)
def _timed_process(self, *args, **kwargs):
"""Track Processor execution time for benchmarking."""
for processor in self._processors:
start_time = _time.process_time()
processor.process(*args, **kwargs)
process_time = int(round((_time.process_time() - start_time) * 1000, 2))
self.process_times[processor.__class__.__name__] = process_time
def process(self, *args, **kwargs):
"""Call the process method on all Processors, in order of their priority.
Call the *process* method on all assigned Processors, respecting their
optional priority setting. In addition, any Entities that were marked
for deletion since the last call to *World.process*, will be deleted
at the start of this method call.
:param args: Optional arguments that will be passed through to the
*process* method of all Processors.
"""
self._clear_dead_entities()
self._process(*args, **kwargs)

View File

@ -1,3 +1,5 @@
from survival.components.position_component import PositionComponent
from survival.entity_layer import EntityLayer
from survival.player import Player
from survival.tile_layer import TileLayer
@ -6,16 +8,21 @@ class GameMap:
def __init__(self, width, height):
self.width = width
self.height = height
self.player = Player()
self.layers = []
self.layers.append(TileLayer(width, height))
self.tile_layer = TileLayer(width, height)
self.entity_layer = EntityLayer(width, height)
def draw(self, camera):
visible_area = camera.get_visible_area()
for layer in self.layers:
layer.draw(camera, visible_area)
self.player.draw(camera)
self.tile_layer.draw(camera, visible_area)
def update(self, camera, delta, pressed_keys):
self.player.update(delta, pressed_keys)
camera.update(self.player)
def add_entity(self, entity, pos):
self.entity_layer.add_entity(entity, pos.grid_position)
def move_entity(self, from_pos, to_pos):
self.entity_layer.move_entity(from_pos, to_pos)
def remove_entity(self, pos):
self.entity_layer.remove_entity(pos)
def is_colliding(self, pos):
return self.entity_layer.is_colliding(pos)

View File

@ -1,20 +0,0 @@
import pygame
from pygame.rect import Rect
class GameObject:
def __init__(self, pos, texture):
self.pos = pos
self.last_pos = pos
self.texture = pygame.image.load(texture)
self.texture = pygame.transform.scale(self.texture, (64, 64))
self.width = self.texture.get_width()
self.height = self.texture.get_height()
self.velocity = [0, 0]
def draw(self, window):
window.blit(self.texture, self.pos)
def get_rect(self):
return Rect(self.pos[0], self.pos[1], self.width, self.height)

View File

@ -7,25 +7,17 @@ from survival.image import Image
class Player:
def __init__(self):
self.pos = [0, 0]
self.velocity = [0, 0]
self.image = Image('stevenson.png')
self.image.set_scale(2)
self.origin = (0, 0)
self.speed = 30
self.movement_target = [self.pos[0], self.pos[1]]
self.timer = 0
# self.pos = [1024, 512]
# self.velocity = [0, 0]
# self.image = Image('stevenson.png')
# self.image.set_scale(2)
# self.speed = 30
# self.movement_target = [self.pos[0], self.pos[1]]
# self.timer = 0
pass
def draw(self, camera):
if self.is_moving():
if self.velocity[0] == 1:
self.image.origin = (96, 0)
elif self.velocity[0] == -1:
self.image.origin = (64, 0)
elif self.velocity[1] == 1:
self.image.origin = (0, 0)
else:
self.image.origin = (32, 0)
self.image.pos = self.pos
camera.draw(self.image)
@ -59,7 +51,7 @@ class Player:
self.timer += delta
if self.timer > 1000:
#self.move_in_random_direction()
self.move_in_random_direction()
self.timer = 0
if pressed_keys[pygame.K_LEFT]:

View File

@ -0,0 +1,23 @@
from survival.components.camera_target_component import CameraTargetComponent
from survival.components.input_component import InputComponent
from survival.components.movement_component import MovementComponent
from survival.components.position_component import PositionComponent
from survival.components.sprite_component import SpriteComponent
class PlayerGenerator:
def create_player(self, world, game_map):
player = world.create_entity()
pos = PositionComponent([0, 0], [0, 0])
world.add_component(player, pos)
world.add_component(player, MovementComponent())
world.add_component(player, InputComponent())
camera_target = CameraTargetComponent(pos)
world.add_component(player, camera_target)
game_map.add_entity(player, pos)
sprite = SpriteComponent('stevenson.png')
sprite.set_scale(1)
world.add_component(player, sprite)
return player

View File

@ -0,0 +1,31 @@
import random
from survival.components.position_component import PositionComponent
from survival.components.sprite_component import SpriteComponent
from survival.settings import RESOURCES_AMOUNT
class ResourceGenerator:
def __init__(self, world, game_map):
self.world = world
self.map = game_map
def generate_resources(self):
for x in range(RESOURCES_AMOUNT):
obj = self.world.create_entity()
sprites = ['apple.png', 'water.png', 'wood.png', 'stone.png']
empty_grid_pos = self.get_empty_grid_position()
empty_pos = [empty_grid_pos[0] * 32, empty_grid_pos[1] * 32]
pos = PositionComponent(empty_pos, empty_grid_pos)
sprite = SpriteComponent(random.choice(sprites))
self.world.add_component(obj, pos)
self.world.add_component(obj, sprite)
self.map.add_entity(obj, pos)
def get_empty_grid_position(self):
free_pos = [random.randrange(self.map.width), random.randrange(self.map.height)]
while self.map.is_colliding(free_pos):
free_pos = [random.randrange(self.map.width), random.randrange(self.map.height)]
return free_pos

View File

@ -1,2 +1,3 @@
SCREEN_WIDTH = 1920
SCREEN_HEIGHT = 1080
RESOURCES_AMOUNT = 300

View File

@ -0,0 +1,12 @@
from survival import esper
from survival.components.camera_target_component import CameraTargetComponent
from survival.components.position_component import PositionComponent
class CameraSystem(esper.Processor):
def __init__(self, camera):
self.camera = camera
def process(self, dt):
for ent, (camera_target, pos) in self.world.get_components(CameraTargetComponent, PositionComponent):
self.camera.update(pos)

View File

@ -0,0 +1,25 @@
from survival import esper
from survival.components.moving_component import MovingComponent
from survival.components.position_component import PositionComponent
class CollisionSystem(esper.Processor):
def __init__(self, game_map):
self.map = game_map
def process(self, dt):
for ent, (pos, moving) in self.world.get_components(PositionComponent, MovingComponent):
if moving.checked_collision:
continue
moving.checked_collision = True
if self.check_collision(moving.movement_target):
self.world.remove_component(ent, MovingComponent)
else:
self.map.move_entity(pos.grid_position, moving.movement_target)
pos.grid_position = moving.movement_target
def check_collision(self, pos):
return self.map.is_colliding(pos)

View File

@ -0,0 +1,13 @@
from survival import esper
from survival.components.position_component import PositionComponent
from survival.components.sprite_component import SpriteComponent
class DrawSystem(esper.Processor):
def __init__(self, camera):
self.camera = camera
def process(self, dt):
for ent, (sprite, pos) in self.world.get_components(SpriteComponent, PositionComponent):
sprite.image.pos = pos.position
self.camera.draw(sprite.image)

View File

@ -0,0 +1,26 @@
import pygame
from survival import esper
from survival.components.input_component import InputComponent
from survival.components.moving_component import MovingComponent
from survival.components.position_component import PositionComponent
class InputSystem(esper.Processor):
def __init__(self):
self.map = None
def process(self, dt):
for ent, (inp, pos) in self.world.get_components(InputComponent, PositionComponent):
keys = pygame.key.get_pressed()
if self.world.has_component(ent, MovingComponent):
continue
if keys[pygame.K_LEFT]:
self.world.add_component(ent, MovingComponent([-1, 0], [pos.grid_position[0] - 1, pos.grid_position[1]]))
elif keys[pygame.K_RIGHT]:
self.world.add_component(ent, MovingComponent([1, 0], [pos.grid_position[0] + 1, pos.grid_position[1]]))
elif keys[pygame.K_DOWN]:
self.world.add_component(ent, MovingComponent([0, 1], [pos.grid_position[0], pos.grid_position[1] + 1]))
elif keys[pygame.K_UP]:
self.world.add_component(ent, MovingComponent([0, -1], [pos.grid_position[0], pos.grid_position[1] - 1]))

View File

@ -0,0 +1,35 @@
from survival import esper
from survival.components.movement_component import MovementComponent
from survival.components.moving_component import MovingComponent
from survival.components.position_component import PositionComponent
from survival.components.sprite_component import SpriteComponent
class MovementSystem(esper.Processor):
def __init__(self):
self.map = None
def process(self, dt):
for ent, (mov, pos, moving, sprite) in self.world.get_components(MovementComponent, PositionComponent,
MovingComponent,
SpriteComponent):
if moving.direction[0] != 0:
pos.position[0] += moving.direction[0] * mov.speed * dt / 100
if abs(moving.movement_target[0] * 32 - pos.position[0]) < 0.1 * mov.speed:
pos.position = [moving.movement_target[0] * 32, moving.movement_target[1] * 32]
self.world.remove_component(ent, MovingComponent)
else:
pos.position[1] += moving.direction[1] * mov.speed * dt / 100
if abs(pos.position[1] - moving.movement_target[1] * 32) < 0.1 * mov.speed:
pos.position = [moving.movement_target[0] * 32, moving.movement_target[1] * 32]
self.world.remove_component(ent, MovingComponent)
if moving.direction[0] == 1:
sprite.image.origin = (96, 0)
elif moving.direction[0] == -1:
sprite.image.origin = (64, 0)
elif moving.direction[1] == 1:
sprite.image.origin = (0, 0)
else:
sprite.image.origin = (32, 0)

View File

@ -10,8 +10,11 @@ class TileLayer:
self.image = Image('atlas.png')
def draw(self, camera, visible_area):
for y in range(int(visible_area.top/32), int(visible_area.height/32) + 1):
for x in range(int(visible_area.left/32), int(visible_area.width/32) + 1):
for y in range(int(visible_area.top / 32), int(visible_area.height / 32) + 1):
for x in range(int(visible_area.left / 32), int(visible_area.width / 32) + 1):
if y >= self.height or x >= self.width:
continue
self.image.pos = (x * 32, y * 32)
self.image.origin = self.tiles[y][x].origin
camera.draw(self.image)

View File

@ -0,0 +1,19 @@
from survival import esper
from survival.systems.camera_system import CameraSystem
from survival.systems.collision_system import CollisionSystem
from survival.systems.draw_system import DrawSystem
from survival.systems.input_system import InputSystem
from survival.systems.movement_system import MovementSystem
class WorldGenerator:
def create_world(self, camera, game_map):
world = esper.World()
world.add_processor(InputSystem())
world.add_processor(CameraSystem(camera))
world.add_processor(MovementSystem(), priority=1)
world.add_processor(CollisionSystem(game_map), priority=2)
world.add_processor(DrawSystem(camera))
return world