Table of Contents
cs20ps23as05: Circle World Game Actor
Goals
- Practice with object-oriented design in Python, implementing a class that models the state of an actor in a simple 2D game, with a variety of special methods and other methods.
- Get a preview of one style of writing GUIs in Python, if you'd like.
Prerequisites
This assignment requires familiarity with the lecture materials presented in class through week 05.
Background
Game Engines and Actors
Quoth the arbiter of all human information:
A game engine is a software framework primarily designed for the development of video games, and generally includes relevant libraries and support programs.
Many game engines are “component-based”, i.e. the state of a game is made up of “actors” present somewhere in the world of the game, which are made up of components. For example, in the Unreal Engine:
Actors are instances of classes that derive from theAActor
class; the base class of all gameplay objects that can be placed into the world. … In general,Actor
s can be thought of as whole items or entities, whileObject
s are more specialized parts. Actors often make use ofComponent
s, which are specializedObject
s, to define certain aspects of their functionality or hold values for a collection of properties. Take a car as an example. The car as a whole is anActor
, whereas the parts of the car, like the wheels and doors, would all beComponent
s of thatActor
.
Discrete-Event Simulations
Most simple video games are a form of discrete-event simulation in which the state of the game's world and its actors are updated for every frame of animation.
Assignment
You shall define a class named CircleActor
in a module named circle_actor
that models some of the basic properties and actions of an actor in a 2D game engine, specifically similar to a cell in the game Agar.io. The state of a CircleActor
could, for example, be updated for every frame of animation in the game, thus making a game like this a discrete-event simulation.
The world in which CircleActor
s exist is a two-dimensional world with a Cartesian coordinate
system, i.e. each actor exists at a particular x/y coordinate and has a certain radius, thereby
covering a certain area of the world.
Your class must be defined as follows in the specifications below.
CircleActor Class Specifications
Please see the following skeleton for the expected methods in class CircleActor
. Feel free to add other methods and import standard-library modules if you'd like.
- circle_actor.py
""" Module for the CircleActor class. """ __author__ = 'A student in CS 20P, someone@jeff.cis.cabrillo.edu' class CircleActor: """ Behaves as a circle in a 2D world centered on an X/Y coordinate. """ def __init__(self, name: str, radius: float, world_size: tuple[float, float], position: tuple[float, float], velocity: tuple[float, float]): """ Constructs a new CircleActor with a given name at a position and with an inherent velocity. :param name: the name of the actor, assumed to be non-empty :param radius: the initial radius of the actor :param world_size: the width and height of the world in which the actor resides :param position: the x/y coordinate of the center of the actor :param velocity: the amount in x/y dimensions by which the actor will move on each step """ # TODO def __bool__(self) -> bool: """ Returns True if this actor is still "alive", meaning its radius is small enough for the circle to fit within the world and no less than 1. """ # TODO def __contains__(self, other) -> bool: """ Returns True if another actor is "contained within" this one, i.e. whether the two actors overlap at all and this actor has a larger radius than the other. """ # TODO def __repr__(self) -> str: """ Returns a printable representation of this actor, appropriate for eval(). That is, the return value of this method should be a string that is valid code for re-constructing this actor with the same attributes. """ # TODO def __str__(self) -> str: """ Returns the name of this actor. """ # TODO def __sub__(self, other) -> float: """ Returns the distance between this actor and another, i.e. how far the two circles are from touching. This value will be negative if the two circles overlap. """ # TODO def collide(self, other): """ "Collides" this actor with another. If they overlap, the radius of the larger actor shall increase by 1 and that of the smaller will decrease by 1. """ # TODO def position(self, new_position: tuple[float, float] = None): """ Given no arguments, returns this actor's position. Given a tuple[float, float] as an argument, sets this actor's x/y position components. """ # TODO def radius(self, new_radius: float = None): """ Given no arguments, returns this actor's radius. Given a real number as an argument, sets this actor's radius. """ # TODO def step(self): """ Moves this actor in in the direction of its velocity by one unit of "time", i.e. one frame of animation or one discrete event. e.g. if position is (4, 5) and velocity is (-1, 1), the new position will be (3, 6). """ # TODO def velocity(self, new_velocity: tuple[float, float] = None): """ Given no arguments, returns this actor's velocity. Given a tuple[float, float] as an argument, sets this actor's x/y velocity components. """ # TODO
Coordinate System, World Boundaries, and Staying In Bounds
The coordinates that make up an actor's location are in an image-like coordinate system, where the origin (0, 0) is in the upper-left corner, the X dimension increases toward the right, and the Y dimension increases toward the bottom. The following image demonstrates how this would work in a 1920×1080 “world”. I have also placed to circles in the image to demonstrate how you should think about the distance between two actors. The position of an actor represents the center of its circle. Finding the distance between those points is trivial. Finding the distance between the two circles simply involves subsequently incorporating the radii.
Several of the methods in class CircleActor
may result in placing an actor in a position where the actor's area extends beyond the boundaries of the world.
If any part of an actor has traveled outside the world, ensure the following for each edge of the world crossed:
- Modify the actor's position to “pull” it back into the world at the exact edge it had traveled over.
- Reverse the horizontal or vertical component of the velocity so that the actor will “bounce” off of the edge instead of traveling over it.
GUI Driver/Tester and Video
I have also provided a GUI for this assignment that will create instances of your CircleActor
class and watch them as they travel around the world and interact with other actors. Note that this GUI uses the tkinter
library module, which offers an interface to the Tcl/Tk toolkit. If you want to run this, you have two major options:
- Run it on your own machine, assuming you have Python installed and it supports
tkinter
. - Run it on the server, with X11 forwarding enabled such that the GUI code will run on the server, but you can interact with the GUI from your own machine.
- MobaXterm supports this automatically, or at least should.
- Add the
-Y
flag when connecting using thessh
command:ssh -Y @jeff.cis.cabrillo.edu
- On macOS, you will first need to install XQuartz in order for this to work.
- On GNU/Linux, this should work fine without additional configuration.
- This should probably work on Windows Terminal as well, if you have the appropriate Linux subsystem services installed.
- circle_world.py
#!/usr/bin/env python """ Quick and dirty Circle World GUI using tkinter """ __author__ = 'Jeffrey Bergamini for CS 20P, jeffrey.bergamini@cabrillo.edu' import circle_actor import random import tkinter import time WINDOW_TITLE = 'CS 20P Circle World' WIDTH = 1280 HEIGHT = 720 FRAME_RATE = 60 STUDENTS = ('adcuthbertson', 'agbrunetto', 'ajgrushkin', 'altorresmoran', 'asmorales', 'atspencer', 'csantiago', 'djrobertson', 'ecweiler', 'fjmorales', 'gehult', 'ikhorvath', 'jebentley', 'jnchristofferso', 'kkonrad', 'lnkleijnen', 'mkowalski', 'mmozer', 'nekoehler', 'nepedrotti', 'rrojasresendiz', 'wsschaffer', 'ymoralesgalvan', 'ysmohamed') DEFAULT_RADIUS = min(WIDTH, HEIGHT) // len(STUDENTS) // 2 MAX_DEFAULT_VELOCITY_COMPONENT = 2 def create_actors(canvas): actors = [] for name in STUDENTS: radius = random.randint(DEFAULT_RADIUS // 2, DEFAULT_RADIUS * 2) actor = circle_actor.CircleActor( name, radius=radius, world_size=(WIDTH, HEIGHT), position=(random.randint(DEFAULT_RADIUS, WIDTH - DEFAULT_RADIUS), random.randint(DEFAULT_RADIUS, HEIGHT - DEFAULT_RADIUS)), velocity=(random.random() * MAX_DEFAULT_VELOCITY_COMPONENT * 2 - MAX_DEFAULT_VELOCITY_COMPONENT, random.random() * MAX_DEFAULT_VELOCITY_COMPONENT * 2 - MAX_DEFAULT_VELOCITY_COMPONENT)) actors.append(actor) posx, posy = actor.position() color_int = int.from_bytes(bytes(0xff - random.randint(0, 127) for _ in range(3)), byteorder='big') actor.oval = canvas.create_oval(posx - radius, posy - radius, posx + radius - 1, posy + radius - 1, fill=f'#{color_int:06x}') actor.label = canvas.create_text(posx, posy, text=str(actor), fill='black', font='Helvetica 12') return actors def animate(window, canvas, actors): while True: # Revive the dead for actor in actors: if not actor: actor.radius(random.randint(DEFAULT_RADIUS // 2, DEFAULT_RADIUS * 2)) actor.position((random.randint(DEFAULT_RADIUS, WIDTH - DEFAULT_RADIUS), random.randint(DEFAULT_RADIUS, HEIGHT - DEFAULT_RADIUS))) actor.velocity( (random.random() * MAX_DEFAULT_VELOCITY_COMPONENT * 2 - MAX_DEFAULT_VELOCITY_COMPONENT, random.random() * MAX_DEFAULT_VELOCITY_COMPONENT * 2 - MAX_DEFAULT_VELOCITY_COMPONENT)) # All actors move one step for actor in actors: actor.step() # They may collide with each other and respond for actor in actors: for other in actors: if actor is not other: actor.collide(other) # Sort the stacking order by size for actor in sorted(actors, key=circle_actor.CircleActor.radius, reverse=True): canvas.lift(actor.oval) canvas.lift(actor.label) # Display them for actor in actors: posx, posy = actor.position() radius = actor.radius() canvas.coords(actor.oval, round(posx - radius), round(posy - radius), round(posx + radius + 1), round(posy + radius + 1)) canvas.coords(actor.label, round(posx), round(posy)) window.update() time.sleep(1 / FRAME_RATE) if __name__ == '__main__': _window = tkinter.Tk() _window.title(WINDOW_TITLE) _window.geometry(f'{WIDTH}x{HEIGHT}') _canvas = tkinter.Canvas(_window) _canvas.configure(bg='white') _canvas.pack(fill='both', expand=True) _actors = create_actors(_canvas) animate(_window, _canvas, _actors)
The following is a short video of the GUI running using my CircleActor
implementation:
Submission
Submit circle_actor.py
via turnin.
Feedback Robot
This project has a feedback robot that will run some tests on your submission and provide you with a feedback report via email within roughly one minute.
Please read the feedback carefully.
Due Date and Point Value
Due at 23:59:59 on the date listed on the syllabus.
Assignment 05
is worth 60 points.
Possible point values per category: --------------------------------------- Properly implemented methods 60 (split as evenly as possible) Possible deductions: Style and practices 10–20% Possible extra credit: Submission via Git 5% ---------------------------------------