User Tools

Site Tools


cs20ps23as05

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 the AActor class; the base class of all gameplay objects that can be placed into the world. … In general, Actors can be thought of as whole items or entities, while Objects are more specialized parts. Actors often make use of Components, which are specialized Objects, 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 an Actor, whereas the parts of the car, like the wheels and doors, would all be Components of that Actor.

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

Agar.io image from Google Play store

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 CircleActors 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.

Image demonstrating the coordinate system of a 1920x1080 "world" in this assignment.

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:

  1. Modify the actor's position to “pull” it back into the world at the exact edge it had traveled over.
  2. 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:

  1. Run it on your own machine, assuming you have Python installed and it supports tkinter.
  2. 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.
    1. MobaXterm supports this automatically, or at least should.
    2. Add the -Y flag when connecting using the ssh command: ssh -Y @jeff.cis.cabrillo.edu
      1. On macOS, you will first need to install XQuartz in order for this to work.
      2. On GNU/Linux, this should work fine without additional configuration.
      3. 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%
---------------------------------------
cs20ps23as05.txt · Last modified: 2023-08-17 10:43 by 127.0.0.1