_images/arcade-logo.svg

The Python Arcade Library

Alternative text Get Started Here

Examples


Installation

Tutorials


Social

example games icon

Games

API icon

API

download icon

Distribute Your Game

source icon

Source Code

Performance

learn icon

Learn

Related Libraries

Quick Arcade Library Introduction Video

Arcade is an easy-to-learn Python library for creating 2D video games. It is ideal for people learning to program, or developers that want to code a 2D game without learning a complex framework.

Get Started Here

_images/treasure-map.svg

Installation

Arcade can be installed like any other Python Package. Arcade needs support for OpenGL 3.3+. It does not run on Raspberry Pi or Wayland. If on Linux, sound support needs at least GLIB 2.29+. For detailed instructions see Installation Instructions.

Starting Tutorials

If you are already familiar with basic Python programming, follow the Simple Platformer or Real Python article. If you are just learning how to program, see the Learn Arcade book.

Arcade Skill Tree

_images/tree.svg

Installation Instructions

Arcade runs on Windows, Mac OS X, and Linux.

Arcade requires Python 3.6 or newer. It does not run on Python 2.x.

Select the instructions for your platform:

Setting Up a Virtual Environment In PyCharm

A Python virtual environment (venv) allows libraries to be installed for just a single project, rather than shared across everyone using the computer. It also does not require administrator privilages to install.

Assuming you already have a project, follow these steps to create a venv:

Step 1: Select File…Settings

_images/file_settings.png

Step 2: Click “Project Interpreter”. Then find the gear icon in the upper right. click on it and select “Add”

_images/click_gear.png

Step 3: Select Virtualenv Environment from the left. Then create a new environment. Usually it should be in a folder called venv in your main project. PyCharm does not always select the correct location by default, so carefully look at the path to make sure it is correct, then select “Ok”.

_images/name_venv.png

Now a virtual environment has been set up. The standard in Python projects is to create a file called requirements.txt and list the packages you want in there.

PyCharm will automatically ask if you want to install those packages as soon as you type them in. Go ahead and let it.

_images/requirements.png

Installation on Windows

To develop with the Arcade library, we need to install Python, then install Arcade.

Step 1: Install Python

Install Python from the official Python website:

https://www.python.org/downloads/

When installing Python, make sure to add Python to the path (1) before clicking the Install button (2).

_images/setup_windows_1.png

After that, you can just close the dialog. There’s no need to increase the path length, although it doesn’t hurt anything if you do.

_images/setup_windows_2.png
Step 2: Install The Arcade Library

If you install Arcade as a pre-built library, there are two options on how to do it. The best way is to use a “virtual environment.” This is a collection of Python libraries that only apply to your particular project. You don’t have to worry about libraries for other projects conflicting with your project. You also don’t need “administrator” level privileges to install libraries.

Install Arcade with PyCharm and a Virtual Environment

If you are using PyCharm, setting up a virtual environment is easy. Once you’ve got your project, open up the settings:

_images/venv_setup_1.png

Select project interpreter:

_images/venv_setup_2.png

Create a new virtual environment. Make sure the venv is inside your project folder.

_images/venv_setup_3.png

Now you can install libraries. PyCharm will automatically ask to add them if you have a file called requirements.txt that lists the required libraries.

_images/venv_setup_4.png

Note

If you are using Python 3.8, the “Numpy” and “Pillow” libraries might try to build themselves from scratch, which will probably error out. To fix, we can manually update “pip” by opening a a PyCharm terminal, and then typing the following into the terminal:

python -m pip install -U --force-reinstall pip

Restart PyCharm (or exit and restart the terminal) and then attempt to install Arcade again.

Install Arcade using the command line interface

If you prefer to use the command line interface (CLI), then you can install arcade directly using pip:

pip3 install arcade

If you happen to be using pipenv, then the appropriate command is:

python3 -m pipenv install arcade

Installation on the Mac

Go to the Python website and download Python.

_images/mac1.png

Then install it:

_images/mac4.png

Download and install PyCharm. The community edition is free, and WAY better than IDLE.

Download the zip file (or use git) for the Arcade template file.

https://github.com/pythonarcade/template

_images/mac5.png

After you’ve downloaded it, open up the zip file, and pull out the template folder to your desktop or whereever you’d like to save it. Then rename it to your project name.

Start PyCharm, and select File…Open and select the folder you just created.

When creating opening the new project, create a virtual environment like so:

_images/mac2.png

If that doesn’t work, (sometimes PyCharm seems to ignore that, or maybe that step got skipped) go into PyCharm…settings, then “Project interpreter” on the right side, click the easy-to-miss gear icon and “Add”

_images/mac6.png

…Then set it like so:

_images/mac3.png

You should get a warning at the top of the screen that ‘arcade’ is not installed. Go ahead and install it. Then try running the starting template.

Installation on Linux

Ubuntu Instructions

The Arcade library is Python 3.6+ only. Most versions of Linux come with Python 2.x. You’ll need to install Python 3 and use it instead of the built-in Python 2.x. (Usually on Linux and Mac, you can type python3 instead of python once installed. Same with pip3 instead of pip to install packages to Python 3.x)

Install Python 3 and some image dependencies:

apt update && sudo apt install -y python3-dev python3-pip libjpeg-dev zlib1g-dev

Check that you have at least Python 3.6 with:

python3 -V

You need at least version 3.6. If you have that, you should be ready to install Arcade:

sudo pip3 install arcade
Raspberry Pi Instructions

Arcade required OpenGL graphics 3.3 or higher. Unfortunately the Raspberry Pi does not support this, Arcade can not run on the Raspberry Pi.

Installation From Source

First step is to clone the repository:

git clone https://github.com/pvcraven/arcade.git

Or download from:

Next, we’ll create a linked install. This will allow you to change files in the arcade directory, and is great if you want to modify the Arcade library code. From the root directory of arcade type:

pip install -e .

To install additional documentation and development requirements:

pip install -e .[dev,docs]
# .. or separately
pip install -e .[dev]
pip install -e .[docs]

Pygame Comparison

The Python Arcade Library has the same target audience as the well-known Pygame library. So how do they differ?

Features that the Arcade Library has:

  • Draws stationary sprites much faster. See Drawing Stationary Sprites

  • Supports Python 3 type hinting.

  • Thick ellipses, arcs, and circles do not have a moiré pattern.

  • Ellipses, arcs, and other shapes can be easily rotated.

  • Uses standard coordinate system you learned about in math. (0, 0) is in the lower left, and not upper left. Y-coordinates are not reversed.

  • Has built-in physics engine for platformers.

  • Supports animated sprites.

  • API documentation for the commands is better.

  • Command names are consistent. For example, to add to a sprite list you use the append() method, like any other list in Python. Pygame uses add().

  • Parameter and command names are clearer. For example, open_window instead of set_mode.

  • Less boiler-plate code than Pygame.

  • Basic drawing does not require knowledge on how to define functions or classes or how to do loops.

  • Encourages separation of logic and display code. Pygame tends to put both into the same game loop.

  • Runs on top of OpenGL 3+ and Pyglet, rather than the old SDL1 library. (Currently PyGame is in the process of moving to SDL2.)

  • With the use of sprite lists, uses the acceleration of the graphics card to improve performance.

  • Easily scale and rotate sprites and graphics.

  • Images with transparency are transparent by default. No extra code needed.

  • Lots of How-To Example Code.

Features that Pygame has that the Arcade Library does not:

  • Has better performance for moving sprites

  • Python 2 support

  • Does not require OpenGL, so works on Raspberry Pis

  • Has better support for pixel manipulation in a memory buffer that isn’t displayed on screen.

Things that are just different:

  • Sound support: Pygame uses the old, unsupported Avbin library. Arcade uses SoLoud. Supports panning and volume.

Arcade Performance Information.

Sample Games Made With Arcade

Have a sample game you’d like to share here? E-mail paul@cravenfamily.com.

Ready or Not?

Ready or Not? a local multiplayer action RPG by Akash S Panickar.

Age of Divisiveness

https://raw.githubusercontent.com/chceswieta/age-of-divisiveness/main/resources/promo/city_build.gif

Age of Divisiveness by Patryk Majewski, Krzysztof Szymaniak, Gabriel Wechta, Błażej Wróbel

Multiplayer LAN game with strong Civilization I and old Settlers vibe! Very extensive.

Fishy-Game

https://raw.githubusercontent.com/LiorAvrahami/fishy-game/main/example%20image.png

Fishy Game by LiorAvrahami

Adventure

Adventure GitHub

Transcience Animation

https://raw.githubusercontent.com/SunTzunami/Transience_animation_PyArcade/master/Demo/preview.gif

Transcience Animation

Stellar Arena Demo

Stellar Arena Demo

Battle Bros

https://raw.githubusercontent.com/njbittner/battle-bros-pyarcade/master/battlebros.gif

Battle Bros Mortal Kombat style game.

Rabbit Herder

https://raw.githubusercontent.com/ryancollingwood/arcade-rabbit-herder/master/resources/static/preview.gif

Rabbit Herder, use carrots and potions to herd a rabbit through a maze.

The Great Skeleton War

The Great Skeleton War, an intense tower defense game, where there’s always something new to discover.

Space Typer

_images/space_typer.png

Space Typer - A typing game

FlapPy Bird

https://camo.githubusercontent.com/f0e9f79d083289e7385a9af79231ba9cc07a10dd/68747470733a2f2f692e706f7374696d672e63632f665678394b736b672f53637265656e5f53686f745f323031382d30392d32375f61745f31322e31312e31395f414d2e706e67

FlapPy-Bird - A bird-game clone.

PyOverheadGame

_images/PyOverheadGame.png

PyOverheadGame, a 2D overhead game where you go through several rooms and pick up keys and other objects.

Dungeon

_images/blake.png

Dungeon, explore a maze picking up arrows and coins.

Two Worlds

Two Worlds, a castle adventure with a dungeon and caverns underneath it.

Simpson College Spring 2017 CMSC 150 Course

These games were created by first-semester programming students.

\

How-To Example Code

Quick API Index

The arcade module

Window and View Classes

Classes:

  • NoOpenGLException

  • View

  • Window

Functions:

  • get_screens

  • open_window

Constants and Data Types:

  • MOUSE_BUTTON_LEFT

  • MOUSE_BUTTON_MIDDLE

  • MOUSE_BUTTON_RIGHT

OpenGL Texture Management

Classes:

  • Matrix3x3

  • Texture

Functions:

  • cleanup_texture_cache

  • load_spritesheet

  • load_texture

  • load_texture_pair

  • load_textures

  • make_circle_texture

  • make_soft_circle_texture

  • make_soft_square_texture

  • trim_image

Simple Physics Engines

Classes:

  • PhysicsEnginePlatformer

  • PhysicsEngineSimple

Earclip Collision Detection

Functions:

  • earclip

Draw Text

Classes:

  • CreateText

  • Text

Functions:

  • create_text

  • draw_text

  • draw_text_2

  • get_text_image

  • render_text

Constants and Data Types:

  • DEFAULT_FONT_NAMES

Game Controller Support

Functions:

  • get_game_controllers

  • get_joysticks

Isometric Map Support (incomplete)

Functions:

  • create_isometric_grid_lines

  • isometric_grid_to_screen

  • screen_to_isometric_grid

Sprites

Classes:

  • AnimatedTimeBasedSprite

  • AnimatedTimeSprite

  • AnimatedWalkingSprite

  • AnimationKeyframe

  • PyMunk

  • Sprite

  • SpriteCircle

  • SpriteSolidColor

Functions:

  • get_distance_between_sprites

Constants and Data Types:

  • FACE_DOWN

  • FACE_LEFT

  • FACE_RIGHT

  • FACE_UP

Misc Utility Functions

Functions:

  • lerp

  • lerp_vec

  • rand_angle_360_deg

  • rand_angle_spread_deg

  • rand_in_circle

  • rand_in_rect

  • rand_on_circle

  • rand_on_line

  • rand_vec_magnitude

  • rand_vec_spread_deg

Particle Emitter Simple

Functions:

  • make_burst_emitter

  • make_interval_emitter

Particle

Classes:

  • EternalParticle

  • FadeParticle

  • LifetimeParticle

  • Particle

Functions:

  • clamp

Constants and Data Types:

  • FilenameOrTexture

Loading TMX (Tiled Map Editor) Maps

Functions:

  • get_tilemap_layer

  • process_layer

  • read_tmx

Support for Drawing Commands

Functions:

  • calculate_hit_box_points_detailed

  • calculate_hit_box_points_simple

  • get_four_byte_color

  • get_four_float_color

  • get_points_for_thick_line

  • make_transparent_color

  • rotate_point

Sprite Lists

Classes:

  • SpriteList

Functions:

  • check_for_collision

  • check_for_collision_with_list

  • get_closest_sprite

  • get_sprites_at_exact_point

  • get_sprites_at_point

Particle Emitter

Classes:

  • EmitBurst

  • EmitController

  • EmitInterval

  • EmitMaintainCount

  • Emitter

  • EmitterIntervalWithCount

  • EmitterIntervalWithTime

Pathfinding Support

Classes:

  • AStarBarrierList

Functions:

  • astar_calculate_path

  • has_line_of_sight

Arcade Version Number

Constants and Data Types:

  • VERSION

Buffered Draw Commands

Classes:

  • Shape

  • ShapeElementList

Functions:

  • create_ellipse

  • create_ellipse_filled

  • create_ellipse_filled_with_colors

  • create_ellipse_outline

  • create_line

  • create_line_generic

  • create_line_generic_with_colors

  • create_line_loop

  • create_line_strip

  • create_lines

  • create_lines_with_colors

  • create_polygon

  • create_rectangle

  • create_rectangle_filled

  • create_rectangle_filled_with_colors

  • create_rectangle_outline

  • create_rectangles_filled_with_colors

  • create_triangles_filled_with_colors

  • get_rectangle_points

Constants and Data Types:

  • TShape

OpenGL Context

Classes:

  • ArcadeContext

Pymunk Physics Engine

Classes:

  • PymunkPhysicsEngine

  • PymunkPhysicsObject

Geometry Support

Functions:

  • are_polygons_intersecting

  • get_distance

  • is_point_in_polygon

Arcade Data Types

Constants and Data Types:

  • Color

  • NamedPoint

  • Point

  • PointList

  • RGB

  • RGBA

  • Rect

  • RectList

  • Vector

Sound Support

Classes:

  • Sound

Functions:

  • load_sound

  • play_sound

  • stop_sound

Drawing Primitives

Functions:

  • draw_arc_filled

  • draw_arc_outline

  • draw_circle_filled

  • draw_circle_outline

  • draw_ellipse_filled

  • draw_ellipse_outline

  • draw_line

  • draw_line_strip

  • draw_lines

  • draw_lrtb_rectangle_filled

  • draw_lrtb_rectangle_outline

  • draw_lrwh_rectangle_textured

  • draw_parabola_filled

  • draw_parabola_outline

  • draw_point

  • draw_points

  • draw_polygon_filled

  • draw_polygon_outline

  • draw_rectangle_filled

  • draw_rectangle_outline

  • draw_scaled_texture_rectangle

  • draw_texture_rectangle

  • draw_triangle_filled

  • draw_triangle_outline

  • draw_xywh_rectangle_filled

  • draw_xywh_rectangle_outline

  • get_image

  • get_pixel

Window Commands

Functions:

  • close_window

  • create_orthogonal_projection

  • finish_render

  • get_display_size

  • get_projection

  • get_scaling_factor

  • get_viewport

  • get_window

  • pause

  • quick_run

  • run

  • schedule

  • set_background_color

  • set_viewport

  • set_window

  • start_render

  • unschedule

The arcade.gl module

OpenGL Texture

Classes:

  • Texture

OpenGL Vertex Array (VAO)

Classes:

  • Geometry

  • VertexArray

Constants and Data Types:

  • index_types

OpenGL Utils

Functions:

  • data_to_ctypes

OpenGL GLSL

Classes:

  • ShaderSource

OpenGL Types

Classes:

  • AttribFormat

  • BufferDescription

  • GLTypes

  • TypeInfo

Constants and Data Types:

  • SHADER_TYPE_NAMES

  • pixel_formats

OpenGL Program

Classes:

  • Program

OpenGL Context

Classes:

  • Context

  • ContextStats

  • Limits

OpenGL Geometry

Functions:

  • cube

  • quad_2d

  • quad_2d_fs

  • screen_rectangle

OpenGL Exceptions

Classes:

  • ShaderException

OpenGL Enums

Constants and Data Types:

  • BLEND_ADDITIVE

  • BLEND_DEFAULT

  • BLEND_PREMULTIPLIED_ALPHA

  • CLAMP_TO_BORDER

  • CLAMP_TO_EDGE

  • DST_ALPHA

  • DST_COLOR

  • FUNC_ADD

  • FUNC_REVERSE_SUBTRACT

  • FUNC_SUBTRACT

  • LINEAR

  • LINEAR_MIPMAP_LINEAR

  • LINEAR_MIPMAP_NEAREST

  • LINES

  • LINES_ADJACENCY

  • LINE_STRIP

  • LINE_STRIP_ADJACENCY

  • MAX

  • MIN

  • MIRRORED_REPEAT

  • NEAREST

  • NEAREST_MIPMAP_LINEAR

  • NEAREST_MIPMAP_NEAREST

  • ONE

  • ONE_MINUS_DST_ALPHA

  • ONE_MINUS_DST_COLOR

  • ONE_MINUS_SRC_ALPHA

  • ONE_MINUS_SRC_COLOR

  • PATCHES

  • POINTS

  • REPEAT

  • SRC_ALPHA

  • SRC_COLOR

  • TRIANGLES

  • TRIANGLES_ADJACENCY

  • TRIANGLE_FAN

  • TRIANGLE_STRIP

  • TRIANGLE_STRIP_ADJACENCY

  • ZERO

OpenGL Uniform Data

Classes:

  • Uniform

  • UniformBlock

OpenGL Query

Classes:

  • Query

OpenGL Buffer

Classes:

  • Buffer

OpenGL FrameBuffer

Classes:

  • DefaultFrameBuffer

  • Framebuffer

The arcade.gui module

GUI Manager

Classes:

  • UIManager

GUI Utilities

Functions:

  • add_margin

  • get_image_with_text

  • get_text_image

  • parse_value

  • render_text_image

GUI Style

Classes:

  • UIStyle

Core GUI

Classes:

  • UIElement

  • UIEvent

  • UIException

Constants and Data Types:

  • KEY_PRESS

  • KEY_RELEASE

  • MOUSE_MOTION

  • MOUSE_PRESS

  • MOUSE_RELEASE

  • MOUSE_SCROLL

  • TEXT_INPUT

  • TEXT_MOTION

  • TEXT_MOTION_SELECTION

Arcade Package API

This page documents the Application Programming Interface (API) for the Python Arcade library. Soo also:

arcade.key package

Mapping of keyboard keys to values.

"""
Constants used to signify what keys on the keyboard were pressed.
"""

# Key modifiers
# Done in powers of two, so you can do a bit-wise 'and' to detect
# multiple modifiers.
MOD_SHIFT = 1
MOD_CTRL = 2
MOD_ALT = 4
MOD_CAPSLOCK = 8
MOD_NUMLOCK = 16
MOD_WINDOWS = 32
MOD_COMMAND = 64
MOD_OPTION = 128
MOD_SCROLLLOCK = 256
MOD_ACCEL = 2

# Keys
BACKSPACE = 65288
TAB = 65289
LINEFEED = 65290
CLEAR = 65291
RETURN = 65293
ENTER = 65293
PAUSE = 65299
SCROLLLOCK = 65300
SYSREQ = 65301
ESCAPE = 65307
HOME = 65360
LEFT = 65361
UP = 65362
RIGHT = 65363
DOWN = 65364
PAGEUP = 65365
PAGEDOWN = 65366
END = 65367
BEGIN = 65368
DELETE = 65535
SELECT = 65376
PRINT = 65377
EXECUTE = 65378
INSERT = 65379
UNDO = 65381
REDO = 65382
MENU = 65383
FIND = 65384
CANCEL = 65385
HELP = 65386
BREAK = 65387
MODESWITCH = 65406
SCRIPTSWITCH = 65406
MOTION_UP = 65362
MOTION_RIGHT = 65363
MOTION_DOWN = 65364
MOTION_LEFT = 65361
MOTION_NEXT_WORD = 1
MOTION_PREVIOUS_WORD = 2
MOTION_BEGINNING_OF_LINE = 3
MOTION_END_OF_LINE = 4
MOTION_NEXT_PAGE = 65366
MOTION_PREVIOUS_PAGE = 65365
MOTION_BEGINNING_OF_FILE = 5
MOTION_END_OF_FILE = 6
MOTION_BACKSPACE = 65288
MOTION_DELETE = 65535
NUMLOCK = 65407
NUM_SPACE = 65408
NUM_TAB = 65417
NUM_ENTER = 65421
NUM_F1 = 65425
NUM_F2 = 65426
NUM_F3 = 65427
NUM_F4 = 65428
NUM_HOME = 65429
NUM_LEFT = 65430
NUM_UP = 65431
NUM_RIGHT = 65432
NUM_DOWN = 65433
NUM_PRIOR = 65434
NUM_PAGE_UP = 65434
NUM_NEXT = 65435
NUM_PAGE_DOWN = 65435
NUM_END = 65436
NUM_BEGIN = 65437
NUM_INSERT = 65438
NUM_DELETE = 65439
NUM_EQUAL = 65469
NUM_MULTIPLY = 65450
NUM_ADD = 65451
NUM_SEPARATOR = 65452
NUM_SUBTRACT = 65453
NUM_DECIMAL = 65454
NUM_DIVIDE = 65455

# Numbers on the numberpad
NUM_0 = 65456
NUM_1 = 65457
NUM_2 = 65458
NUM_3 = 65459
NUM_4 = 65460
NUM_5 = 65461
NUM_6 = 65462
NUM_7 = 65463
NUM_8 = 65464
NUM_9 = 65465

F1 = 65470
F2 = 65471
F3 = 65472
F4 = 65473
F5 = 65474
F6 = 65475
F7 = 65476
F8 = 65477
F9 = 65478
F10 = 65479
F11 = 65480
F12 = 65481
F13 = 65482
F14 = 65483
F15 = 65484
F16 = 65485
LSHIFT = 65505
RSHIFT = 65506
LCTRL = 65507
RCTRL = 65508
CAPSLOCK = 65509
LMETA = 65511
RMETA = 65512
LALT = 65513
RALT = 65514
LWINDOWS = 65515
RWINDOWS = 65516
LCOMMAND = 65517
RCOMMAND = 65518
LOPTION = 65488
ROPTION = 65489
SPACE = 32
EXCLAMATION = 33
DOUBLEQUOTE = 34
HASH = 35
POUND = 35
DOLLAR = 36
PERCENT = 37
AMPERSAND = 38
APOSTROPHE = 39
PARENLEFT = 40
PARENRIGHT = 41
ASTERISK = 42
PLUS = 43
COMMA = 44
MINUS = 45
PERIOD = 46
SLASH = 47

# Numbers on the main keyboard
KEY_0 = 48
KEY_1 = 49
KEY_2 = 50
KEY_3 = 51
KEY_4 = 52
KEY_5 = 53
KEY_6 = 54
KEY_7 = 55
KEY_8 = 56
KEY_9 = 57
COLON = 58
SEMICOLON = 59
LESS = 60
EQUAL = 61
GREATER = 62
QUESTION = 63
AT = 64
BRACKETLEFT = 91
BACKSLASH = 92
BRACKETRIGHT = 93
ASCIICIRCUM = 94
UNDERSCORE = 95
GRAVE = 96
QUOTELEFT = 96
A = 97
B = 98
C = 99
D = 100
E = 101
F = 102
G = 103
H = 104
# noinspection PyPep8
I = 105
J = 106
K = 107
L = 108
M = 109
N = 110
# noinspection PyPep8
O = 111
P = 112
Q = 113
R = 114
S = 115
T = 116
U = 117
V = 118
W = 119
X = 120
Y = 121
Z = 122
BRACELEFT = 123
BAR = 124
BRACERIGHT = 125
ASCIITILDE = 126

arcade.csscolor package

These are standard CSS named colors you can use when drawing.

You can specify colors four ways:

  • Standard CSS color names (this package): arcade.csscolor.RED

  • Nonstandard color names arcade.color package: arcade.color.RED

  • Three-byte numbers: (255, 0, 0)

  • Four-byte numbers (fourth byte is transparency. 0 transparent, 255 opaque): (255, 0, 0, 255)

ALICE_BLUE(240, 248, 255) 
ANTIQUE_WHITE(250, 235, 215) 
AQUA(0, 255, 255) 
AQUAMARINE(127, 255, 212) 
AZURE(240, 255, 255) 
BEIGE(245, 245, 220) 
BISQUE(255, 228, 196) 
BLACK(0, 0, 0) 
BLANCHED_ALMOND(255, 235, 205) 
BLUE(0, 0, 255) 
BLUE_VIOLET(138, 43, 226) 
BROWN(165, 42, 42) 
BURLYWOOD(222, 184, 135) 
CADET_BLUE(95, 158, 160) 
CHARTREUSE(127, 255, 0) 
CHOCOLATE(210, 105, 30) 
CORAL(255, 127, 80) 
CORNFLOWER_BLUE(100, 149, 237) 
CORNSILK(255, 248, 220) 
CRIMSON(220, 20, 60) 
CYAN(0, 255, 255) 
DARK_BLUE(0, 0, 139) 
DARK_CYAN(0, 139, 139) 
DARK_GOLDENROD(184, 134, 11) 
DARK_GRAY(169, 169, 169) 
DARK_GREEN(0, 100, 0) 
DARK_GREY(169, 169, 169) 
DARK_KHAKI(189, 183, 107) 
DARK_MAGENTA(139, 0, 139) 
DARK_OLIVE_GREEN(85, 107, 47) 
DARK_ORANGE(255, 140, 0) 
DARK_ORCHID(153, 50, 204) 
DARK_RED(139, 0, 0) 
DARK_SALMON(233, 150, 122) 
DARK_SEA_GREEN(143, 188, 143) 
DARK_SLATE_BLUE(72, 61, 139) 
DARK_SLATE_GRAY(47, 79, 79) 
DARK_SLATE_GREY(47, 79, 79) 
DARK_TURQUOISE(0, 206, 209) 
DARK_VIOLET(148, 0, 211) 
DEEP_PINK(255, 20, 147) 
DEEP_SKY_BLUE(0, 191, 255) 
DIM_GRAY(105, 105, 105) 
DIM_GREY(105, 105, 105) 
DODGER_BLUE(30, 144, 255) 
FIREBRICK(178, 34, 34) 
FLORAL_WHITE(255, 250, 240) 
FOREST_GREEN(34, 139, 34) 
FUCHSIA(255, 0, 255) 
GAINSBORO(220, 220, 220) 
GHOST_WHITE(248, 248, 255) 
GOLD(255, 215, 0) 
GOLDENROD(218, 165, 32) 
GRAY(128, 128, 128) 
GREEN(0, 128, 0) 
GREENYELLOW(173, 255, 47) 
GREY(128, 128, 128) 
HONEYDEW(240, 255, 240) 
HOTPINK(255, 105, 180) 
INDIANRED(205, 92, 92) 
INDIGO(75, 0, 130) 
IVORY(255, 255, 240) 
KHAKI(240, 230, 140) 
LAVENDER(230, 230, 250) 
LAVENDER_BLUSH(255, 240, 245) 
LAWNGREEN(124, 252, 0) 
LEMON_CHIFFON(255, 250, 205) 
LIGHT_BLUE(173, 216, 230) 
LIGHT_CORAL(240, 128, 128) 
LIGHT_CYAN(224, 255, 255) 
LIGHT_GOLDENROD_YELLOW(250, 250, 210) 
LIGHT_GRAY(211, 211, 211) 
LIGHT_GREEN(144, 238, 144) 
LIGHT_GREY(211, 211, 211) 
LIGHT_PINK(255, 182, 193) 
LIGHT_SALMON(255, 160, 122) 
LIGHT_SEA_GREEN(32, 178, 170) 
LIGHT_SKY_BLUE(135, 206, 250) 
LIGHT_SLATE_GRAY(119, 136, 153) 
LIGHT_SLATE_GREY(119, 136, 153) 
LIGHT_STEEL_BLUE(176, 196, 222) 
LIGHT_YELLOW(255, 255, 224) 
LIME(0, 255, 0) 
LIME_GREEN(50, 205, 50) 
LINEN(250, 240, 230) 
MAGENTA(255, 0, 255) 
MAROON(128, 0, 0) 
MEDIUM_AQUAMARINE(102, 205, 170) 
MEDIUM_BLUE(0, 0, 205) 
MEDIUM_ORCHID(186, 85, 211) 
MEDIUM_PURPLE(147, 112, 219) 
MEDIUM_SEA_GREEN(60, 179, 113) 
MEDIUM_SLATE_BLUE(123, 104, 238) 
MEDIUM_SPRING_GREEN(0, 250, 154) 
MEDIUM_TURQUOISE(72, 209, 204) 
MEDIUM_VIOLET_RED(199, 21, 133) 
MIDNIGHT_BLUE(25, 25, 112) 
MINT_CREAM(245, 255, 250) 
MISTY_ROSE(255, 228, 225) 
MOCCASIN(255, 228, 181) 
NAVAJO_WHITE(255, 222, 173) 
NAVY(0, 0, 128) 
OLD_LACE(253, 245, 230) 
OLIVE(128, 128, 0) 
OLIVE_DRAB(107, 142, 35) 
ORANGE(255, 165, 0) 
ORANGE_RED(255, 69, 0) 
ORCHID(218, 112, 214) 
PALE_GOLDENROD(238, 232, 170) 
PALE_GREEN(152, 251, 152) 
PALE_TURQUOISE(175, 238, 238) 
PALE_VIOLET_RED(219, 112, 147) 
PAPAYA_WHIP(255, 239, 213) 
PEACH_PUFF(255, 218, 185) 
PERU(205, 133, 63) 
PINK(255, 192, 203) 
PLUM(221, 160, 221) 
POWDER_BLUE(176, 224, 230) 
PURPLE(128, 0, 128) 
RED(255, 0, 0) 
ROSY_BROWN(188, 143, 143) 
ROYAL_BLUE(65, 105, 225) 
SADDLE_BROWN(139, 69, 19) 
SALMON(250, 128, 114) 
SANDY_BROWN(244, 164, 96) 
SEA_GREEN(46, 139, 87) 
SEASHELL(255, 245, 238) 
SIENNA(160, 82, 45) 
SILVER(192, 192, 192) 
SKY_BLUE(135, 206, 235) 
SLATE_BLUE(106, 90, 205) 
SLATE_GRAY(112, 128, 144) 
SLATE_GREY(112, 128, 144) 
SNOW(255, 250, 250) 
SPRING_GREEN(0, 255, 127) 
STEEL_BLUE(70, 130, 180) 
TAN(210, 180, 140) 
TEAL(0, 128, 128) 
THISTLE(216, 191, 216) 
TOMATO(255, 99, 71) 
TURQUOISE(64, 224, 208) 
VIOLET(238, 130, 238) 
WHEAT(245, 222, 179) 
WHITE(255, 255, 255) 
WHITE_SMOKE(245, 245, 245) 
YELLOW(255, 255, 0) 
YELLOW_GREEN(154, 205) 

arcade.color package

These are named colors you can use when drawing.

You can specify colors four ways:

  • Standard CSS color names arcade.csscolor package: arcade.csscolor.RED

  • Nonstandard color names (this package): arcade.color.RED

  • Three-byte numbers: (255, 0, 0)

  • Four-byte numbers (fourth byte is transparency. 0 transparent, 255 opaque): (255, 0, 0, 255)

AERO_BLUE(201, 255, 229) 
AFRICAN_VIOLET(178, 132, 190) 
AIR_FORCE_BLUE(93, 138, 168) 
AIR_SUPERIORITY_BLUE(114, 160, 193) 
ALABAMA_CRIMSON(175, 0, 42) 
ALICE_BLUE(240, 248, 255) 
ALIZARIN_CRIMSON(227, 38, 54) 
ALLOY_ORANGE(196, 98, 16) 
ALMOND(239, 222, 205) 
AMARANTH(229, 43, 80) 
AMARANTH_PINK(241, 156, 187) 
AMARANTH_PURPLE(171, 39, 79) 
AMAZON(59, 122, 87) 
AMBER(255, 191, 0) 
SAE(255, 126, 0) 
AMERICAN_ROSE(255, 3, 62) 
AMETHYST(153, 102, 204) 
ANDROID_GREEN(164, 198, 57) 
ANTI_FLASH_WHITE(242, 243, 244) 
ANTIQUE_BRASS(205, 149, 117) 
ANTIQUE_BRONZE(102, 93, 30) 
ANTIQUE_FUCHSIA(145, 92, 131) 
ANTIQUE_RUBY(132, 27, 45) 
ANTIQUE_WHITE(250, 235, 215) 
AO(0, 128, 0) 
APPLE_GREEN(141, 182, 0) 
APRICOT(251, 206, 177) 
AQUA(0, 255, 255) 
AQUAMARINE(127, 255, 212) 
ARMY_GREEN(75, 83, 32) 
ARSENIC(59, 68, 75) 
ARTICHOKE(143, 151, 121) 
ARYLIDE_YELLOW(233, 214, 107) 
ASH_GREY(178, 190, 181) 
ASPARAGUS(135, 169, 107) 
ATOMIC_TANGERINE(255, 153, 102) 
AUBURN(165, 42, 42) 
AUREOLIN(253, 238, 0) 
AUROMETALSAURUS(110, 127, 128) 
AVOCADO(86, 130, 3) 
AZURE(0, 127, 255) 
AZURE_MIST(240, 255, 255) 
BABY_BLUE(137, 207, 240) 
BABY_BLUE_EYES(161, 202, 241) 
BABY_PINK(244, 194, 194) 
BABY_POWDER(254, 254, 250) 
BAKER_MILLER_PINK(255, 145, 175) 
BALL_BLUE(33, 171, 205) 
BANANA_MANIA(250, 231, 181) 
BANANA_YELLOW(255, 225, 53) 
BANGLADESH_GREEN(0, 106, 78) 
BARBIE_PINK(224, 33, 138) 
BARN_RED(124, 10, 2) 
BATTLESHIP_GREY(132, 132, 130) 
BAZAAR(152, 119, 123) 
BEAU_BLUE(188, 212, 230) 
BRIGHT_LILAC(216, 145, 239) 
BEAVER(159, 129, 112) 
BEIGE(245, 245, 220) 
BISQUE(255, 228, 196) 
BISTRE(61, 43, 31) 
BISTRE_BROWN(150, 113, 23) 
BITTER_LEMON(202, 224, 13) 
BITTER_LIME(100, 140, 17) 
BITTERSWEET(254, 111, 94) 
BITTERSWEET_SHIMMER(191, 79, 81) 
BLACK(0, 0, 0) 
BLACK_BEAN(61, 12, 2) 
BLACK_LEATHER_JACKET(37, 53, 41) 
BLACK_OLIVE(59, 60, 54) 
BLANCHED_ALMOND(255, 235, 205) 
BLAST_OFF_BRONZE(165, 113, 100) 
BLEU_DE_FRANCE(49, 140, 231) 
BLIZZARD_BLUE(172, 229, 238) 
BLOND(250, 240, 190) 
BLUE(0, 0, 255) 
BLUE_BELL(162, 162, 208) 
BLUE_GRAY(102, 153, 204) 
BLUE_GREEN(13, 152, 186) 
BLUE_SAPPHIRE(18, 97, 128) 
BLUE_VIOLET(138, 43, 226) 
BLUE_YONDER(80, 114, 167) 
BLUEBERRY(79, 134, 247) 
BLUEBONNET(28, 28, 240) 
BLUSH(222, 93, 131) 
BOLE(121, 68, 59) 
BONDI_BLUE(0, 149, 182) 
BONE(227, 218, 201) 
BOSTON_UNIVERSITY_RED(204, 0, 0) 
BOTTLE_GREEN(0, 106, 78) 
BOYSENBERRY(135, 50, 96) 
BRANDEIS_BLUE(0, 112, 255) 
BRASS(181, 166, 66) 
BRICK_RED(203, 65, 84) 
BRIGHT_CERULEAN(29, 172, 214) 
BRIGHT_GREEN(102, 255, 0) 
BRIGHT_LAVENDER(191, 148, 228) 
BRIGHT_MAROON(195, 33, 72) 
BRIGHT_NAVY_BLUE(25, 116, 210) 
BRIGHT_PINK(255, 0, 127) 
BRIGHT_TURQUOISE(8, 232, 222) 
BRIGHT_UBE(209, 159, 232) 
BRILLIANT_LAVENDER(244, 187, 255) 
BRILLIANT_ROSE(255, 85, 163) 
BRINK_PINK(251, 96, 127) 
BRITISH_RACING_GREEN(0, 66, 37) 
BRONZE(205, 127, 50) 
BRONZE_YELLOW(115, 112, 0) 
BROWN(165, 42, 42) 
BROWN_NOSE(107, 68, 35) 
BRUNSWICK_GREEN(27, 77, 62) 
BUBBLE_GUM(255, 193, 204) 
BUBBLES(231, 254, 255) 
BUD_GREEN(123, 182, 97) 
BUFF(240, 220, 130) 
BULGARIAN_ROSE(72, 6, 7) 
BURGUNDY(128, 0, 32) 
BURLYWOOD(222, 184, 135) 
BURNT_ORANGE(204, 85, 0) 
BURNT_SIENNA(233, 116, 81) 
BURNT_UMBER(138, 51, 36) 
BYZANTINE(189, 51, 164) 
BYZANTIUM(112, 41, 99) 
CADET(83, 104, 114) 
CADET_BLUE(95, 158, 160) 
CADET_GREY(145, 163, 176) 
CADMIUM_GREEN(0, 107, 60) 
CADMIUM_ORANGE(237, 135, 45) 
CADMIUM_RED(227, 0, 34) 
CADMIUM_YELLOW(255, 246, 0) 
CAL_POLY_GREEN(30, 77, 43) 
CAMBRIDGE_BLUE(163, 193, 173) 
CAMEL(193, 154, 107) 
CAMEO_PINK(239, 187, 204) 
CAMOUFLAGE_GREEN(120, 134, 107) 
CANARY_YELLOW(255, 239, 0) 
CANDY_APPLE_RED(255, 8, 0) 
CANDY_PINK(228, 113, 122) 
CAPRI(0, 191, 255) 
CAPUT_MORTUUM(89, 39, 32) 
CARDINAL(196, 30, 58) 
CARIBBEAN_GREEN(0, 204, 153) 
CARMINE(150, 0, 24) 
CARMINE_PINK(235, 76, 66) 
CARMINE_RED(255, 0, 56) 
CARNATION_PINK(255, 166, 201) 
CARNELIAN(179, 27, 27) 
CAROLINA_BLUE(153, 186, 221) 
CARROT_ORANGE(237, 145, 33) 
CASTLETON_GREEN(0, 86, 63) 
CATALINA_BLUE(6, 42, 120) 
CATAWBA(112, 54, 66) 
CEDAR_CHEST(201, 90, 73) 
CEIL(146, 161, 207) 
CELADON(172, 225, 175) 
CELADON_BLUE(0, 123, 167) 
CELADON_GREEN(47, 132, 124) 
CELESTE(178, 255, 255) 
CELESTIAL_BLUE(73, 151, 208) 
CERISE(222, 49, 99) 
CERISE_PINK(236, 59, 131) 
CERULEAN(0, 123, 167) 
CERULEAN_BLUE(42, 82, 190) 
CERULEAN_FROST(109, 155, 195) 
CG_BLUE(0, 122, 165) 
CG_RED(224, 60, 49) 
CHAMOISEE(160, 120, 90) 
CHAMPAGNE(247, 231, 206) 
CHARCOAL(54, 69, 79) 
CHARLESTON_GREEN(35, 43, 43) 
CHARM_PINK(230, 143, 172) 
CHARTREUSE(127, 255, 0) 
CHERRY(222, 49, 99) 
CHERRY_BLOSSOM_PINK(255, 183, 197) 
CHESTNUT(149, 69, 53) 
CHINA_PINK(222, 111, 161) 
CHINA_ROSE(168, 81, 110) 
CHINESE_RED(170, 56, 30) 
CHINESE_VIOLET(133, 96, 136) 
CHOCOLATE(210, 105, 30) 
CHROME_YELLOW(255, 167, 0) 
CINEREOUS(152, 129, 123) 
CINNABAR(227, 66, 52) 
CINNAMON(210, 105, 30) 
CITRINE(228, 208, 10) 
CITRON(159, 169, 31) 
CLARET(127, 23, 52) 
CLASSIC_ROSE(251, 204, 231) 
COAL(124, 185, 232) 
COBALT(0, 71, 171) 
COCOA_BROWN(210, 105, 30) 
COCONUT(150, 90, 62) 
COFFEE(111, 78, 55) 
COLUMBIA_BLUE(155, 221, 255) 
CONGO_PINK(248, 131, 121) 
COOL_BLACK(0, 46, 99) 
COOL_GREY(140, 146, 172) 
COPPER(184, 115, 51) 
COPPER_PENNY(173, 111, 105) 
COPPER_RED(203, 109, 81) 
COPPER_ROSE(153, 102, 102) 
COQUELICOT(255, 56, 0) 
CORAL(255, 127, 80) 
CORAL_PINK(248, 131, 121) 
CORAL_RED(255, 64, 64) 
CORDOVAN(137, 63, 69) 
CORN(251, 236, 93) 
CORNELL_RED(179, 27, 27) 
CORNFLOWER_BLUE(100, 149, 237) 
CORNSILK(255, 248, 220) 
COSMIC_LATTE(255, 248, 231) 
COTTON_CANDY(255, 188, 217) 
CREAM(255, 253, 208) 
CRIMSON(220, 20, 60) 
CRIMSON_GLORY(190, 0, 50) 
CYAN(0, 255, 255) 
CYBER_GRAPE(88, 66, 124) 
CYBER_YELLOW(255, 211, 0) 
DAFFODIL(255, 255, 49) 
DANDELION(240, 225, 48) 
DARK_BLUE(0, 0, 139) 
DARK_BLUE_GRAY(102, 102, 153) 
DARK_BROWN(101, 67, 33) 
DARK_BYZANTIUM(93, 57, 84) 
DARK_CANDY_APPLE_RED(164, 0, 0) 
DARK_CERULEAN(8, 69, 126) 
DARK_CHESTNUT(152, 105, 96) 
DARK_CORAL(205, 91, 69) 
DARK_CYAN(0, 139, 139) 
DARK_ELECTRIC_BLUE(83, 104, 120) 
DARK_GOLDENROD(184, 134, 11) 
DARK_GRAY(169, 169, 169) 
DARK_GREEN(1, 50, 32) 
DARK_IMPERIAL_BLUE(0, 65, 106) 
DARK_JUNGLE_GREEN(26, 36, 33) 
DARK_KHAKI(189, 183, 107) 
DARK_LAVA(72, 60, 50) 
DARK_LAVENDER(115, 79, 150) 
DARK_LIVER(83, 75, 79) 
DARK_MAGENTA(139, 0, 139) 
DARK_MIDNIGHT_BLUE(0, 51, 102) 
DARK_MOSS_GREEN(74, 93, 35) 
DARK_OLIVE_GREEN(85, 107, 47) 
DARK_ORANGE(255, 140, 0) 
DARK_ORCHID(153, 50, 204) 
DARK_PASTEL_BLUE(119, 158, 203) 
DARK_PASTEL_GREEN(3, 192, 60) 
DARK_PASTEL_PURPLE(150, 111, 214) 
DARK_PASTEL_RED(194, 59, 34) 
DARK_PINK(231, 84, 128) 
DARK_POWDER_BLUE(0, 51, 153) 
DARK_PUCE(79, 58, 60) 
DARK_RASPBERRY(135, 38, 87) 
DARK_RED(139, 0, 0) 
DARK_SALMON(233, 150, 122) 
DARK_SCARLET(86, 3, 25) 
DARK_SEA_GREEN(143, 188, 143) 
DARK_SIENNA(60, 20, 20) 
DARK_SKY_BLUE(140, 190, 214) 
DARK_SLATE_BLUE(72, 61, 139) 
DARK_SLATE_GRAY(47, 79, 79) 
DARK_SPRING_GREEN(23, 114, 69) 
DARK_TAN(145, 129, 81) 
DARK_TANGERINE(255, 168, 18) 
DARK_TAUPE(72, 60, 50) 
DARK_TERRA_COTTA(204, 78, 92) 
DARK_TURQUOISE(0, 206, 209) 
DARK_VANILLA(209, 190, 168) 
DARK_VIOLET(148, 0, 211) 
DARK_YELLOW(155, 135, 12) 
DARTMOUTH_GREEN(0, 112, 60) 
DAVY_GREY(85, 85, 85) 
DEBIAN_RED(215, 10, 83) 
DEEP_CARMINE(169, 32, 62) 
DEEP_CARMINE_PINK(239, 48, 56) 
DEEP_CARROT_ORANGE(233, 105, 44) 
DEEP_CERISE(218, 50, 135) 
DEEP_CHAMPAGNE(250, 214, 165) 
DEEP_CHESTNUT(185, 78, 72) 
DEEP_COFFEE(112, 66, 65) 
DEEP_FUCHSIA(193, 84, 193) 
DEEP_JUNGLE_GREEN(0, 75, 73) 
DEEP_LEMON(245, 199, 26) 
DEEP_LILAC(153, 85, 187) 
DEEP_MAGENTA(204, 0, 204) 
DEEP_MAUVE(212, 115, 212) 
DEEP_MOSS_GREEN(53, 94, 59) 
DEEP_PEACH(255, 203, 164) 
DEEP_PINK(255, 20, 147) 
DEEP_PUCE(169, 92, 104) 
DEEP_RUBY(132, 63, 91) 
DEEP_SAFFRON(255, 153, 51) 
DEEP_SKY_BLUE(0, 191, 255) 
DEEP_SPACE_SPARKLE(74, 100, 108) 
DEEP_TAUPE(126, 94, 96) 
DEEP_TUSCAN_RED(102, 66, 77) 
DEER(186, 135, 89) 
DENIM(21, 96, 189) 
DESERT(193, 154, 107) 
DESERT_SAND(237, 201, 175) 
DESIRE(234, 60, 83) 
DIAMOND(185, 242, 255) 
DIM_GRAY(105, 105, 105) 
DIRT(155, 118, 83) 
DODGER_BLUE(30, 144, 255) 
DOGWOOD_ROSE(215, 24, 104) 
DOLLAR_BILL(133, 187, 101) 
DONKEY_BROWN(102, 76, 40) 
DRAB(150, 113, 23) 
DUKE_BLUE(0, 0, 156) 
DUST_STORM(229, 204, 201) 
DUTCH_WHITE(239, 223, 187) 
EARTH_YELLOW(225, 169, 95) 
EBONY(85, 93, 80) 
ECRU(194, 178, 128) 
EERIE_BLACK(27, 27, 27) 
EGGPLANT(97, 64, 81) 
EGGSHELL(240, 234, 214) 
EGYPTIAN_BLUE(16, 52, 166) 
ELECTRIC_BLUE(125, 249, 255) 
ELECTRIC_CRIMSON(255, 0, 63) 
ELECTRIC_CYAN(0, 255, 255) 
ELECTRIC_GREEN(0, 255, 0) 
ELECTRIC_INDIGO(111, 0, 255) 
ELECTRIC_LAVENDER(244, 187, 255) 
ELECTRIC_LIME(204, 255, 0) 
ELECTRIC_PURPLE(191, 0, 255) 
ELECTRIC_ULTRAMARINE(63, 0, 255) 
ELECTRIC_VIOLET(143, 0, 255) 
ELECTRIC_YELLOW(255, 255, 0) 
EMERALD(80, 200, 120) 
EMINENCE(108, 48, 130) 
ENGLISH_GREEN(27, 77, 62) 
ENGLISH_LAVENDER(180, 131, 149) 
ENGLISH_RED(171, 75, 82) 
ENGLISH_VIOLET(86, 60, 92) 
ETON_BLUE(150, 200, 162) 
EUCALYPTUS(68, 215, 168) 
FALLOW(193, 154, 107) 
FALU_RED(128, 24, 24) 
FANDANGO(181, 51, 137) 
FANDANGO_PINK(222, 82, 133) 
FASHION_FUCHSIA(244, 0, 161) 
FAWN(229, 170, 112) 
FELDGRAU(77, 93, 83) 
FELDSPAR(253, 213, 177) 
FERN_GREEN(79, 121, 66) 
FERRARI_RED(255, 40, 0) 
FIELD_DRAB(108, 84, 30) 
FIREBRICK(178, 34, 34) 
FIRE_ENGINE_RED(206, 32, 41) 
FLAME(226, 88, 34) 
FLAMINGO_PINK(252, 142, 172) 
FLATTERY(107, 68, 35) 
FLAVESCENT(247, 233, 142) 
FLAX(238, 220, 130) 
FLIRT(162, 0, 109) 
FLORAL_WHITE(255, 250, 240) 
FLUORESCENT_ORANGE(255, 191, 0) 
FLUORESCENT_PINK(255, 20, 147) 
FLUORESCENT_YELLOW(204, 255, 0) 
FOLLY(255, 0, 79) 
FOREST_GREEN(34, 139, 34) 
FRENCH_BEIGE(166, 123, 91) 
FRENCH_BISTRE(133, 109, 77) 
FRENCH_BLUE(0, 114, 187) 
FRENCH_FUCHSIA(253, 63, 146) 
FRENCH_LILAC(134, 96, 142) 
FRENCH_LIME(158, 253, 56) 
FRENCH_MAUVE(212, 115, 212) 
FRENCH_PINK(253, 108, 158) 
FRENCH_PUCE(78, 22, 9) 
FRENCH_RASPBERRY(199, 44, 72) 
FRENCH_ROSE(246, 74, 138) 
FRENCH_SKY_BLUE(119, 181, 254) 
FRENCH_WINE(172, 30, 68) 
FRESH_AIR(166, 231, 255) 
FUCHSIA(255, 0, 255) 
FUCHSIA_PINK(255, 119, 255) 
FUCHSIA_PURPLE(204, 57, 123) 
FUCHSIA_ROSE(199, 67, 117) 
FULVOUS(228, 132, 0) 
FUZZY_WUZZY(204, 102, 102) 
GAINSBORO(220, 220, 220) 
GAMBOGE(228, 155, 15) 
GENERIC_VIRIDIAN(0, 127, 102) 
GHOST_WHITE(248, 248, 255) 
GIANTS_ORANGE(254, 90, 29) 
GINGER(176, 101, 0) 
GLAUCOUS(96, 130, 182) 
GLITTER(230, 232, 250) 
GO_GREEN(0, 171, 102) 
GOLD(255, 215, 0) 
GOLD_FUSION(133, 117, 78) 
GOLDEN_BROWN(153, 101, 21) 
GOLDEN_POPPY(252, 194, 0) 
GOLDEN_YELLOW(255, 223, 0) 
GOLDENROD(218, 165, 32) 
GRANNY_SMITH_APPLE(168, 228, 160) 
GRAPE(111, 45, 168) 
GRAY(128, 128, 128) 
GRAY_ASPARAGUS(70, 89, 69) 
GRAY_BLUE(140, 146, 172) 
GREEN(0, 255, 0) 
GREEN_YELLOW(173, 255, 47) 
GRULLO(169, 154, 134) 
GUPPIE_GREEN(0, 255, 127) 
HAN_BLUE(68, 108, 207) 
HAN_PURPLE(82, 24, 250) 
HANSA_YELLOW(233, 214, 107) 
HARLEQUIN(63, 255, 0) 
HARVARD_CRIMSON(201, 0, 22) 
HARVEST_GOLD(218, 145, 0) 
HEART_GOLD(128, 128, 0) 
HELIOTROPE(223, 115, 255) 
HELIOTROPE_GRAY(170, 152, 169) 
HOLLYWOOD_CERISE(244, 0, 161) 
HONEYDEW(240, 255, 240) 
HONOLULU_BLUE(0, 109, 176) 
HOOKER_GREEN(73, 121, 107) 
HOT_MAGENTA(255, 29, 206) 
HOT_PINK(255, 105, 180) 
HUNTER_GREEN(53, 94, 59) 
ICEBERG(113, 166, 210) 
ICTERINE(252, 247, 94) 
ILLUMINATING_EMERALD(49, 145, 119) 
IMPERIAL(96, 47, 107) 
IMPERIAL_BLUE(0, 35, 149) 
IMPERIAL_PURPLE(102, 2, 60) 
IMPERIAL_RED(237, 41, 57) 
INCHWORM(178, 236, 93) 
INDEPENDENCE(76, 81, 109) 
INDIA_GREEN(19, 136, 8) 
INDIAN_RED(205, 92, 92) 
INDIAN_YELLOW(227, 168, 87) 
INDIGO(75, 0, 130) 
INTERNATIONAL_KLEIN_BLUE(0, 47, 167) 
INTERNATIONAL_ORANGE(255, 79, 0) 
IRIS(90, 79, 207) 
IRRESISTIBLE(179, 68, 108) 
ISABELLINE(244, 240, 236) 
ISLAMIC_GREEN(0, 144, 0) 
ITALIAN_SKY_BLUE(178, 255, 255) 
IVORY(255, 255, 240) 
JADE(0, 168, 107) 
JAPANESE_CARMINE(157, 41, 51) 
JAPANESE_INDIGO(38, 67, 72) 
JAPANESE_VIOLET(91, 50, 86) 
JASMINE(248, 222, 126) 
JASPER(215, 59, 62) 
JAZZBERRY_JAM(165, 11, 94) 
JELLY_BEAN(218, 97, 78) 
JET(52, 52, 52) 
JONQUIL(244, 202, 22) 
JORDY_BLUE(138, 185, 241) 
JUNE_BUD(189, 218, 87) 
JUNGLE_GREEN(41, 171, 135) 
KELLY_GREEN(76, 187, 23) 
KENYAN_COPPER(124, 28, 5) 
KEPPEL(58, 176, 158) 
KHAKI(195, 176, 145) 
KOBE(136, 45, 23) 
KOBI(231, 159, 196) 
KOMBU_GREEN(53, 66, 48) 
KU_CRIMSON(232, 0, 13) 
LA_SALLE_GREEN(8, 120, 48) 
LANGUID_LAVENDER(214, 202, 221) 
LAPIS_LAZULI(38, 97, 156) 
LASER_LEMON(255, 255, 102) 
LAUREL_GREEN(169, 186, 157) 
LAVA(207, 16, 32) 
LAVENDER(230, 230, 250) 
LAVENDER_BLUE(204, 204, 255) 
LAVENDER_BLUSH(255, 240, 245) 
LAVENDER_GRAY(196, 195, 208) 
LAVENDER_INDIGO(148, 87, 235) 
LAVENDER_MAGENTA(238, 130, 238) 
LAVENDER_MIST(230, 230, 250) 
LAVENDER_PINK(251, 174, 210) 
LAVENDER_PURPLE(150, 123, 182) 
LAVENDER_ROSE(251, 160, 227) 
LAWN_GREEN(124, 252, 0) 
LEMON(255, 247, 0) 
LEMON_CHIFFON(255, 250, 205) 
LEMON_CURRY(204, 160, 29) 
LEMON_GLACIER(253, 255, 0) 
LEMON_LIME(227, 255, 0) 
LEMON_MERINGUE(246, 234, 190) 
LEMON_YELLOW(255, 244, 79) 
LIBERTY(84, 90, 167) 
LICORICE(26, 17, 16) 
LIGHT_APRICOT(253, 213, 177) 
LIGHT_BLUE(173, 216, 230) 
LIGHT_BROWN(181, 101, 29) 
LIGHT_CARMINE_PINK(230, 103, 113) 
LIGHT_CORAL(240, 128, 128) 
LIGHT_CORNFLOWER_BLUE(147, 204, 234) 
LIGHT_CRIMSON(245, 105, 145) 
LIGHT_CYAN(224, 255, 255) 
LIGHT_DEEP_PINK(255, 92, 205) 
LIGHT_FUCHSIA_PINK(249, 132, 239) 
LIGHT_GOLDENROD_YELLOW(250, 250, 210) 
LIGHT_GRAY(211, 211, 211) 
LIGHT_GREEN(144, 238, 144) 
LIGHT_HOT_PINK(255, 179, 222) 
LIGHT_KHAKI(240, 230, 140) 
LIGHT_MEDIUM_ORCHID(211, 155, 203) 
LIGHT_MOSS_GREEN(173, 223, 173) 
LIGHT_ORCHID(230, 168, 215) 
LIGHT_PASTEL_PURPLE(177, 156, 217) 
LIGHT_PINK(255, 182, 193) 
LIGHT_RED_OCHRE(233, 116, 81) 
LIGHT_SALMON(255, 160, 122) 
LIGHT_SALMON_PINK(255, 153, 153) 
LIGHT_SEA_GREEN(32, 178, 170) 
LIGHT_SKY_BLUE(135, 206, 250) 
LIGHT_SLATE_GRAY(119, 136, 153) 
LIGHT_STEEL_BLUE(176, 196, 222) 
LIGHT_TAUPE(179, 139, 109) 
LIGHT_THULIAN_PINK(230, 143, 172) 
LIGHT_YELLOW(255, 255, 224) 
LILAC(200, 162, 200) 
LIME(191, 255, 0) 
LIME_GREEN(50, 205, 50) 
LIMERICK(157, 194, 9) 
LINCOLN_GREEN(25, 89, 5) 
LINEN(250, 240, 230) 
LION(193, 154, 107) 
LISERAN_PURPLE(222, 111, 161) 
LITTLE_BOY_BLUE(108, 160, 220) 
LIVER(103, 76, 71) 
LIVER_CHESTNUT(152, 116, 86) 
LIVID(102, 153, 204) 
LUMBER(255, 228, 205) 
LUST(230, 32, 32) 
MAGENTA(255, 0, 255) 
MAGENTA_HAZE(159, 69, 118) 
MAGIC_MINT(170, 240, 209) 
MAGNOLIA(248, 244, 255) 
MAHOGANY(192, 64, 0) 
MAIZE(251, 236, 93) 
MAJORELLE_BLUE(96, 80, 220) 
MALACHITE(11, 218, 81) 
MANATEE(151, 154, 170) 
MANGO_TANGO(255, 130, 67) 
MANTIS(116, 195, 101) 
MARDI_GRAS(136, 0, 133) 
MAROON(128, 0, 0) 
MAUVE(224, 176, 255) 
MAUVE_TAUPE(145, 95, 109) 
MAUVELOUS(239, 152, 170) 
MAYA_BLUE(115, 194, 251) 
MEAT_BROWN(229, 183, 59) 
MEDIUM_AQUAMARINE(102, 221, 170) 
MEDIUM_BLUE(0, 0, 205) 
MEDIUM_CANDY_APPLE_RED(226, 6, 44) 
MEDIUM_CARMINE(175, 64, 53) 
MEDIUM_CHAMPAGNE(243, 229, 171) 
MEDIUM_ELECTRIC_BLUE(3, 80, 150) 
MEDIUM_JUNGLE_GREEN(28, 53, 45) 
MEDIUM_LAVENDER_MAGENTA(221, 160, 221) 
MEDIUM_ORCHID(186, 85, 211) 
MEDIUM_PERSIAN_BLUE(0, 103, 165) 
MEDIUM_PURPLE(147, 112, 219) 
MEDIUM_RED_VIOLET(187, 51, 133) 
MEDIUM_RUBY(170, 64, 105) 
MEDIUM_SEA_GREEN(60, 179, 113) 
MEDIUM_SLATE_BLUE(123, 104, 238) 
MEDIUM_SPRING_BUD(201, 220, 135) 
MEDIUM_SPRING_GREEN(0, 250, 154) 
MEDIUM_SKY_BLUE(128, 218, 235) 
MEDIUM_TAUPE(103, 76, 71) 
MEDIUM_TURQUOISE(72, 209, 204) 
MEDIUM_TUSCAN_RED(121, 68, 59) 
MEDIUM_VERMILION(217, 96, 59) 
MEDIUM_VIOLET_RED(199, 21, 133) 
MELLOW_APRICOT(248, 184, 120) 
MELLOW_YELLOW(248, 222, 126) 
MELON(253, 188, 180) 
METALLIC_SEAWEED(10, 126, 140) 
METALLIC_SUNBURST(156, 124, 56) 
MEXICAN_PINK(228, 0, 124) 
MIDNIGHT_BLUE(25, 25, 112) 
MIDNIGHT_GREEN(0, 73, 83) 
MIKADO_YELLOW(255, 196, 12) 
MINDARO(227, 249, 136) 
MINT(62, 180, 137) 
MINT_CREAM(245, 255, 250) 
MINT_GREEN(152, 255, 152) 
MISTY_ROSE(255, 228, 225) 
MOCCASIN(250, 235, 215) 
MODE_BEIGE(150, 113, 23) 
MOONSTONE_BLUE(115, 169, 194) 
MOSS_GREEN(138, 154, 91) 
MOUNTAIN_MEADOW(48, 186, 143) 
MOUNTBATTEN_PINK(153, 122, 141) 
MSU_GREEN(24, 69, 59) 
MUGHAL_GREEN(48, 96, 48) 
MULBERRY(197, 75, 140) 
MUSTARD(255, 219, 88) 
MYRTLE_GREEN(49, 120, 115) 
NADESHIKO_PINK(246, 173, 198) 
NAPIER_GREEN(42, 128, 0) 
NAPLES_YELLOW(250, 218, 94) 
NAVAJO_WHITE(255, 222, 173) 
NAVY_BLUE(0, 0, 128) 
NAVY_PURPLE(148, 87, 235) 
NEON_CARROT(255, 163, 67) 
NEON_FUCHSIA(254, 65, 100) 
NEON_GREEN(57, 255, 20) 
NEW_CAR(33, 79, 198) 
NEW_YORK_PINK(215, 131, 127) 
NON_PHOTO_BLUE(164, 221, 237) 
NYANZA(233, 255, 219) 
OCEAN_BOAT_BLUE(0, 119, 190) 
OCHRE(204, 119, 34) 
OFFICE_GREEN(0, 128, 0) 
OLD_BURGUNDY(67, 48, 46) 
OLD_GOLD(207, 181, 59) 
OLD_HELIOTROPE(86, 60, 92) 
OLD_LACE(253, 245, 230) 
OLD_LAVENDER(121, 104, 120) 
OLD_MAUVE(103, 49, 71) 
OLD_MOSS_GREEN(134, 126, 54) 
OLD_ROSE(192, 128, 129) 
OLD_SILVER(132, 132, 130) 
OLIVE(128, 128, 0) 
OLIVE_DRAB(107, 142, 35) 
OLIVINE(154, 185, 115) 
ONYX(53, 56, 57) 
OPERA_MAUVE(183, 132, 167) 
ORANGE(255, 165, 0) 
ORANGE_PEEL(255, 159, 0) 
ORANGE_RED(255, 69, 0) 
ORCHID(218, 112, 214) 
ORCHID_PINK(242, 141, 205) 
ORIOLES_ORANGE(251, 79, 20) 
OTTER_BROWN(101, 67, 33) 
OUTER_SPACE(65, 74, 76) 
OUTRAGEOUS_ORANGE(255, 110, 74) 
OXFORD_BLUE(0, 33, 71) 
OU_CRIMSON_RED(153, 0, 0) 
PAKISTAN_GREEN(0, 102, 0) 
PALATINATE_BLUE(39, 59, 226) 
PALATINATE_PURPLE(104, 40, 96) 
PALE_AQUA(188, 212, 230) 
PALE_BLUE(175, 238, 238) 
PALE_BROWN(152, 118, 84) 
PALE_CARMINE(175, 64, 53) 
PALE_CERULEAN(155, 196, 226) 
PALE_CHESTNUT(221, 173, 175) 
PALE_COPPER(218, 138, 103) 
PALE_CORNFLOWER_BLUE(171, 205, 239) 
PALE_GOLD(230, 190, 138) 
PALE_GOLDENROD(238, 232, 170) 
PALE_GREEN(152, 251, 152) 
PALE_LAVENDER(220, 208, 255) 
PALE_MAGENTA(249, 132, 229) 
PALE_PINK(250, 218, 221) 
PALE_PLUM(221, 160, 221) 
PALE_RED_VIOLET(219, 112, 147) 
PALE_ROBIN_EGG_BLUE(150, 222, 209) 
PALE_SILVER(201, 192, 187) 
PALE_SPRING_BUD(236, 235, 189) 
PALE_TAUPE(188, 152, 126) 
PALE_TURQUOISE(175, 238, 238) 
PALE_VIOLET_RED(219, 112, 147) 
PANSY_PURPLE(120, 24, 74) 
PAOLO_VERONESE_GREEN(0, 155, 125) 
PAPAYA_WHIP(255, 239, 213) 
PARADISE_PINK(230, 62, 98) 
PARIS_GREEN(80, 200, 120) 
PASTEL_BLUE(174, 198, 207) 
PASTEL_BROWN(131, 105, 83) 
PASTEL_GRAY(207, 207, 196) 
PASTEL_GREEN(119, 221, 119) 
PASTEL_MAGENTA(244, 154, 194) 
PASTEL_ORANGE(255, 179, 71) 
PASTEL_PINK(222, 165, 164) 
PASTEL_PURPLE(179, 158, 181) 
PASTEL_RED(255, 105, 97) 
PASTEL_VIOLET(203, 153, 201) 
PASTEL_YELLOW(253, 253, 150) 
PATRIARCH(128, 0, 128) 
PAYNE_GREY(83, 104, 120) 
PEACH(255, 229, 180) 
PEACH_ORANGE(255, 204, 153) 
PEACH_PUFF(255, 218, 185) 
PEACH_YELLOW(250, 223, 173) 
PEAR(209, 226, 49) 
PEARL(234, 224, 200) 
PEARL_AQUA(136, 216, 192) 
PEARLY_PURPLE(183, 104, 162) 
PERIDOT(230, 226, 0) 
PERIWINKLE(204, 204, 255) 
PERSIAN_BLUE(28, 57, 187) 
PERSIAN_GREEN(0, 166, 147) 
PERSIAN_INDIGO(50, 18, 122) 
PERSIAN_ORANGE(217, 144, 88) 
PERSIAN_PINK(247, 127, 190) 
PERSIAN_PLUM(112, 28, 28) 
PERSIAN_RED(204, 51, 51) 
PERSIAN_ROSE(254, 40, 162) 
PERSIMMON(236, 88, 0) 
PERU(205, 133, 63) 
PHLOX(223, 0, 255) 
PHTHALO_BLUE(0, 15, 137) 
PHTHALO_GREEN(18, 53, 36) 
PICTON_BLUE(69, 177, 232) 
PICTORIAL_CARMINE(195, 11, 78) 
PIGGY_PINK(253, 221, 230) 
PINE_GREEN(1, 121, 111) 
PINK(255, 192, 203) 
PINK_LACE(255, 221, 244) 
PINK_LAVENDER(216, 178, 209) 
PINK_PEARL(231, 172, 207) 
PINK_SHERBET(247, 143, 167) 
PISTACHIO(147, 197, 114) 
PLATINUM(229, 228, 226) 
PLUM(221, 160, 221) 
POMP_AND_POWER(134, 96, 142) 
POPSTAR(190, 79, 98) 
PORTLAND_ORANGE(255, 90, 54) 
POWDER_BLUE(176, 224, 230) 
PRINCETON_ORANGE(255, 143, 0) 
PRUNE(112, 28, 28) 
PRUSSIAN_BLUE(0, 49, 83) 
PSYCHEDELIC_PURPLE(223, 0, 255) 
PUCE(204, 136, 153) 
PUCE_RED(114, 47, 55) 
PULLMAN_BROWN(100, 65, 23) 
PUMPKIN(255, 117, 24) 
PURPLE(128, 0, 128) 
PURPLE_HEART(105, 53, 156) 
PURPLE_MOUNTAIN_MAJESTY(150, 120, 182) 
PURPLE_NAVY(78, 81, 128) 
PURPLE_PIZZAZZ(254, 78, 218) 
PURPLE_TAUPE(80, 64, 77) 
PURPUREUS(154, 78, 174) 
QUARTZ(81, 72, 79) 
QUEEN_BLUE(67, 107, 149) 
QUEEN_PINK(232, 204, 215) 
QUINACRIDONE_MAGENTA(142, 58, 89) 
RACKLEY(93, 138, 168) 
RADICAL_RED(255, 53, 94) 
RAJAH(251, 171, 96) 
RASPBERRY(227, 11, 93) 
RASPBERRY_GLACE(145, 95, 109) 
RASPBERRY_PINK(226, 80, 152) 
RASPBERRY_ROSE(179, 68, 108) 
RAW_UMBER(130, 102, 68) 
RAZZLE_DAZZLE_ROSE(255, 51, 204) 
RAZZMATAZZ(227, 37, 107) 
RAZZMIC_BERRY(141, 78, 133) 
RED(255, 0, 0) 
RED_BROWN(165, 42, 42) 
RED_DEVIL(134, 1, 17) 
RED_ORANGE(255, 83, 73) 
RED_PURPLE(228, 0, 120) 
RED_VIOLET(199, 21, 133) 
REDWOOD(164, 90, 82) 
REGALIA(82, 45, 128) 
RESOLUTION_BLUE(0, 35, 135) 
RHYTHM(119, 118, 150) 
RICH_BLACK(0, 64, 64) 
RICH_BRILLIANT_LAVENDER(241, 167, 254) 
RICH_CARMINE(215, 0, 64) 
RICH_ELECTRIC_BLUE(8, 146, 208) 
RICH_LAVENDER(167, 107, 207) 
RICH_LILAC(182, 102, 210) 
RICH_MAROON(176, 48, 96) 
RIFLE_GREEN(68, 76, 56) 
ROAST_COFFEE(112, 66, 65) 
ROBIN_EGG_BLUE(0, 204, 204) 
ROCKET_METALLIC(138, 127, 128) 
ROMAN_SILVER(131, 137, 150) 
ROSE(255, 0, 127) 
ROSE_BONBON(249, 66, 158) 
ROSE_EBONY(103, 72, 70) 
ROSE_GOLD(183, 110, 121) 
ROSE_MADDER(227, 38, 54) 
ROSE_PINK(255, 102, 204) 
ROSE_QUARTZ(170, 152, 169) 
ROSE_RED(194, 30, 86) 
ROSE_TAUPE(144, 93, 93) 
ROSE_VALE(171, 78, 82) 
ROSEWOOD(101, 0, 11) 
ROSSO_CORSA(212, 0, 0) 
ROSY_BROWN(188, 143, 143) 
ROYAL_AZURE(0, 56, 168) 
ROYAL_BLUE(65, 105, 225) 
ROYAL_FUCHSIA(202, 44, 146) 
ROYAL_PURPLE(120, 81, 169) 
ROYAL_YELLOW(250, 218, 94) 
RUBER(206, 70, 118) 
RUBINE_RED(209, 0, 86) 
RUBY(224, 17, 95) 
RUBY_RED(155, 17, 30) 
RUDDY(255, 0, 40) 
RUDDY_BROWN(187, 101, 40) 
RUDDY_PINK(225, 142, 150) 
RUFOUS(168, 28, 7) 
RUSSET(128, 70, 27) 
RUSSIAN_GREEN(103, 146, 103) 
RUSSIAN_VIOLET(50, 23, 77) 
RUST(183, 65, 14) 
RUSTY_RED(218, 44, 67) 
SACRAMENTO_STATE_GREEN(0, 86, 63) 
SADDLE_BROWN(139, 69, 19) 
SAFETY_ORANGE(255, 103, 0) 
SAFETY_YELLOW(238, 210, 2) 
SAFFRON(244, 196, 48) 
SAGE(188, 184, 138) 
ST_PATRICK_BLUE(35, 41, 122) 
SALMON(250, 128, 114) 
SALMON_PINK(255, 145, 164) 
SAND(194, 178, 128) 
SAND_DUNE(150, 113, 23) 
SANDSTORM(236, 213, 64) 
SANDY_BROWN(244, 164, 96) 
SANDY_TAUPE(150, 113, 23) 
SANGRIA(146, 0, 10) 
SAP_GREEN(80, 125, 42) 
SAPPHIRE(15, 82, 186) 
SAPPHIRE_BLUE(0, 103, 165) 
SATIN_SHEEN_GOLD(203, 161, 53) 
SCARLET(255, 36, 0) 
SCHAUSS_PINK(255, 145, 175) 
SCHOOL_BUS_YELLOW(255, 216, 0) 
SCREAMIN_GREEN(118, 255, 122) 
SEA_BLUE(0, 105, 148) 
SEA_GREEN(46, 255, 139) 
SEAL_BROWN(50, 20, 20) 
SEASHELL(255, 245, 238) 
SELECTIVE_YELLOW(255, 186, 0) 
SEPIA(112, 66, 20) 
SHADOW(138, 121, 93) 
SHADOW_BLUE(119, 139, 165) 
SHAMPOO(255, 207, 241) 
SHAMROCK_GREEN(0, 158, 96) 
SHEEN_GREEN(143, 212, 0) 
SHIMMERING_BLUSH(217, 134, 149) 
SHOCKING_PINK(252, 15, 192) 
SIENNA(136, 45, 23) 
SILVER(192, 192, 192) 
SILVER_CHALICE(172, 172, 172) 
SILVER_LAKE_BLUE(93, 137, 186) 
SILVER_PINK(196, 174, 173) 
SILVER_SAND(191, 193, 194) 
SINOPIA(203, 65, 11) 
SKOBELOFF(0, 116, 116) 
SKY_BLUE(135, 206, 235) 
SKY_MAGENTA(207, 113, 175) 
SLATE_BLUE(106, 90, 205) 
SLATE_GRAY(112, 128, 144) 
SMALT(0, 51, 153) 
SMITTEN(200, 65, 134) 
SMOKE(115, 130, 118) 
SMOKEY_TOPAZ(147, 61, 65) 
SMOKY_BLACK(16, 12, 8) 
SNOW(255, 250, 250) 
SOAP(206, 200, 239) 
SONIC_SILVER(117, 117, 117) 
SPACE_CADET(29, 41, 81) 
SPANISH_BISTRE(128, 117, 90) 
SPANISH_CARMINE(209, 0, 71) 
SPANISH_CRIMSON(229, 26, 76) 
SPANISH_BLUE(0, 112, 184) 
SPANISH_GRAY(152, 152, 152) 
SPANISH_GREEN(0, 145, 80) 
SPANISH_ORANGE(232, 97, 0) 
SPANISH_PINK(247, 191, 190) 
SPANISH_RED(230, 0, 38) 
SPANISH_SKY_BLUE(0, 170, 228) 
SPANISH_VIOLET(76, 40, 130) 
SPANISH_VIRIDIAN(0, 127, 92) 
SPIRO_DISCO_BALL(15, 192, 252) 
SPRING_BUD(167, 252, 0) 
SPRING_GREEN(0, 255, 127) 
STAR_COMMAND_BLUE(0, 123, 184) 
STEEL_BLUE(70, 130, 180) 
STEEL_PINK(204, 51, 102) 
STIL_DE_GRAIN_YELLOW(250, 218, 94) 
STIZZA(153, 0, 0) 
STORMCLOUD(79, 102, 106) 
STRAW(228, 217, 111) 
STRAWBERRY(252, 90, 141) 
SUNGLOW(255, 204, 51) 
SUNRAY(227, 171, 87) 
SUNSET(250, 214, 165) 
SUNSET_ORANGE(253, 94, 83) 
SUPER_PINK(207, 107, 169) 
TAN(210, 180, 140) 
TANGELO(249, 77, 0) 
TANGERINE(242, 133, 0) 
TANGERINE_YELLOW(255, 204, 0) 
TANGO_PINK(228, 113, 122) 
TAUPE(72, 60, 50) 
TAUPE_GRAY(139, 133, 137) 
TEA_GREEN(208, 240, 192) 
TEA_ROSE(244, 194, 194) 
TEAL(0, 128, 128) 
TEAL_BLUE(54, 117, 136) 
TEAL_DEER(153, 230, 179) 
TEAL_GREEN(0, 130, 127) 
TELEMAGENTA(207, 52, 118) 
TERRA_COTTA(226, 114, 91) 
THISTLE(216, 191, 216) 
THULIAN_PINK(222, 111, 161) 
TICKLE_ME_PINK(252, 137, 172) 
TIFFANY_BLUE(10, 186, 181) 
TIGERS_EYE(224, 141, 60) 
TIMBERWOLF(219, 215, 210) 
TITANIUM_YELLOW(238, 230, 0) 
TOMATO(255, 99, 71) 
TOOLBOX(116, 108, 192) 
TOPAZ(255, 200, 124) 
TRACTOR_RED(253, 14, 53) 
TROLLEY_GREY(128, 128, 128) 
TROPICAL_RAIN_FOREST(0, 117, 94) 
TRUE_BLUE(0, 115, 207) 
TUFTS_BLUE(65, 125, 193) 
TULIP(255, 135, 141) 
TUMBLEWEED(222, 170, 136) 
TURKISH_ROSE(181, 114, 129) 
TURQUOISE(64, 224, 208) 
TURQUOISE_BLUE(0, 255, 239) 
TURQUOISE_GREEN(160, 214, 180) 
TUSCAN(250, 214, 165) 
TUSCAN_BROWN(111, 78, 55) 
TUSCAN_RED(124, 72, 72) 
TUSCAN_TAN(166, 123, 91) 
TUSCANY(192, 153, 153) 
TWILIGHT_LAVENDER(138, 73, 107) 
TYRIAN_PURPLE(102, 2, 60) 
UA_BLUE(0, 51, 170) 
UA_RED(217, 0, 76) 
UBE(136, 120, 195) 
UCLA_BLUE(83, 104, 149) 
UCLA_GOLD(255, 179, 0) 
UFO_GREEN(60, 208, 112) 
ULTRAMARINE(18, 10, 143) 
ULTRAMARINE_BLUE(65, 102, 245) 
ULTRA_PINK(255, 111, 255) 
UMBER(99, 81, 71) 
UNBLEACHED_SILK(255, 221, 202) 
UNITED_NATIONS_BLUE(91, 146, 229) 
UNIVERSITY_OF_CALIFORNIA_GOLD(183, 135, 39) 
UNMELLOW_YELLOW(255, 255, 102) 
UP_FOREST_GREEN(1, 68, 33) 
UP_MAROON(123, 17, 19) 
UPSDELL_RED(174, 32, 41) 
UROBILIN(225, 173, 33) 
USAFA_BLUE(0, 79, 152) 
USC_CARDINAL(153, 0, 0) 
USC_GOLD(255, 204, 0) 
UNIVERSITY_OF_TENNESSEE_ORANGE(247, 127, 0) 
UTAH_CRIMSON(211, 0, 63) 
VANILLA(243, 229, 171) 
VANILLA_ICE(243, 143, 169) 
VEGAS_GOLD(197, 179, 88) 
VENETIAN_RED(200, 8, 21) 
VERDIGRIS(67, 179, 174) 
VERMILION(227, 66, 52) 
VERONICA(160, 32, 240) 
VIOLET(143, 0, 255) 
VIOLET_BLUE(50, 74, 178) 
VIOLET_RED(247, 83, 148) 
VIRIDIAN(64, 130, 109) 
VIRIDIAN_GREEN(0, 150, 152) 
VIVID_AUBURN(146, 39, 36) 
VIVID_BURGUNDY(159, 29, 53) 
VIVID_CERISE(218, 29, 129) 
VIVID_ORCHID(204, 0, 255) 
VIVID_SKY_BLUE(0, 204, 255) 
VIVID_TANGERINE(255, 160, 137) 
VIVID_VIOLET(159, 0, 255) 
WARM_BLACK(0, 66, 66) 
WATERSPOUT(164, 244, 249) 
WENGE(100, 84, 82) 
WHEAT(245, 222, 179) 
WHITE(255, 255, 255) 
WHITE_SMOKE(245, 245, 245) 
WILD_BLUE_YONDER(162, 173, 208) 
WILD_ORCHID(212, 112, 162) 
WILD_STRAWBERRY(255, 67, 164) 
WILD_WATERMELON(252, 108, 133) 
WILLPOWER_ORANGE(253, 88, 0) 
WINDSOR_TAN(167, 85, 2) 
WINE(114, 47, 55) 
WINE_DREGS(103, 49, 71) 
WISTERIA(201, 160, 220) 
WOOD_BROWN(193, 154, 107) 
XANADU(115, 134, 120) 
YALE_BLUE(15, 77, 146) 
YANKEES_BLUE(28, 40, 65) 
YELLOW(255, 255, 0) 
YELLOW_GREEN(154, 205, 50) 
YELLOW_ORANGE(255, 174, 66) 
YELLOW_ROSE(255, 240, 0) 
ZAFFRE(0, 20, 168) 
ZINNWALDITE_BROWN(44, 22, 8) 

arcade.gl package

Buffer
BufferDescription
Context
Framebuffer
Geometry
Program
Query
Texture
VertexArray
ShaderException
geometry

arcade.gui package

Resources

Resource files are images and sounds built into Arcade that can be used to quickly build and test simple code without having to worry about copying files into the project.

Any file loaded that starts with :resources: will attempt to load that file from the library resources instead of the project directory.

Many of the resources come from Kenney.nl and are licensed under CC0 (Creative Commons Zero). Be sure to check out his web page for a much wider selection of assets.

:resources:images/
:resources:images/isometric_dungeon/
:resources:images/isometric_dungeon/stone_W.png
stone_W.png
:resources:images/isometric_dungeon/tableChairsBroken_S.png
tableChairsBroken_S.png
:resources:images/isometric_dungeon/stone_N.png
stone_N.png
:resources:images/isometric_dungeon/stoneUneven_S.png
stoneUneven_S.png
:resources:images/isometric_dungeon/dirt_S.png
dirt_S.png
:resources:images/isometric_dungeon/stoneWall_N.png
stoneWall_N.png
:resources:images/isometric_dungeon/stoneWallCorner_W.png
stoneWallCorner_W.png
:resources:images/isometric_dungeon/stoneUneven_N.png
stoneUneven_N.png
:resources:images/isometric_dungeon/stone_S.png
stone_S.png
:resources:images/isometric_dungeon/stoneWallAged_E.png
stoneWallAged_E.png
:resources:images/isometric_dungeon/stoneMissingTiles_N.png
stoneMissingTiles_N.png
:resources:images/isometric_dungeon/dirtTiles_S.png
dirtTiles_S.png
:resources:images/isometric_dungeon/stoneWallCorner_N.png
stoneWallCorner_N.png
:resources:images/isometric_dungeon/stoneUneven_W.png
stoneUneven_W.png
:resources:images/isometric_dungeon/stoneTile_W.png
stoneTile_W.png
:resources:images/isometric_dungeon/stoneMissingTiles_S.png
stoneMissingTiles_S.png
:resources:images/isometric_dungeon/stoneTile_N.png
stoneTile_N.png
:resources:images/isometric_dungeon/stoneTile_S.png
stoneTile_S.png
:resources:images/isometric_dungeon/woodenCrates_W.png
woodenCrates_W.png
:resources:images/isometric_dungeon/stoneWall_S.png
stoneWall_S.png
:resources:images/isometric_dungeon/woodenSupportBeams_S.png
woodenSupportBeams_S.png
:resources:images/isometric_dungeon/stoneUneven_E.png
stoneUneven_E.png
:resources:images/isometric_dungeon/stoneWallCorner_S.png
stoneWallCorner_S.png
:resources:images/isometric_dungeon/stoneSideUneven_N.png
stoneSideUneven_N.png
:resources:images/isometric_dungeon/tableShortChairs_W.png
tableShortChairs_W.png
:resources:images/isometric_dungeon/stoneWall_W.png
stoneWall_W.png
:resources:images/isometric_dungeon/stoneWallGateOpen_E.png
stoneWallGateOpen_E.png
:resources:images/isometric_dungeon/tableChairsBroken_E.png
tableChairsBroken_E.png
:resources:images/isometric_dungeon/stoneWallGateClosed_E.png
stoneWallGateClosed_E.png
:resources:images/isometric_dungeon/stoneWallCorner_E.png
stoneWallCorner_E.png
:resources:images/isometric_dungeon/stone_E.png
stone_E.png
:resources:images/isometric_dungeon/stoneWallColumn_E.png
stoneWallColumn_E.png
:resources:images/isometric_dungeon/woodenSupportsBeam_S.png
woodenSupportsBeam_S.png
:resources:images/isometric_dungeon/stoneSide_E.png
stoneSide_E.png
:resources:images/isometric_dungeon/stoneWallArchway_S.png
stoneWallArchway_S.png
:resources:images/isometric_dungeon/stoneMissingTiles_W.png
stoneMissingTiles_W.png
:resources:images/isometric_dungeon/stoneMissingTiles_E.png
stoneMissingTiles_E.png
:resources:images/isometric_dungeon/stoneLeft_N.png
stoneLeft_N.png
:resources:images/isometric_dungeon/stoneWallGateClosed_S.png
stoneWallGateClosed_S.png
:resources:images/isometric_dungeon/stoneWallAged_S.png
stoneWallAged_S.png
:resources:images/topdown_tanks/
:resources:images/topdown_tanks/tankRed_barrel3.png
tankRed_barrel3.png
:resources:images/topdown_tanks/tileGrass_roadCrossing.png
tileGrass_roadCrossing.png
:resources:images/topdown_tanks/tankGreen_barrel1.png
tankGreen_barrel1.png
:resources:images/topdown_tanks/tank_red.png
tank_red.png
:resources:images/topdown_tanks/tileGrass_transitionN.png
tileGrass_transitionN.png
:resources:images/topdown_tanks/tileSand_roadCornerLL.png
tileSand_roadCornerLL.png
:resources:images/topdown_tanks/tankDark_barrel3_outline.png
tankDark_barrel3_outline.png
:resources:images/topdown_tanks/treeBrown_large.png
treeBrown_large.png
:resources:images/topdown_tanks/tracksLarge.png
tracksLarge.png
:resources:images/topdown_tanks/tileGrass_roadTransitionW_dirt.png
tileGrass_roadTransitionW_dirt.png
:resources:images/topdown_tanks/tankRed_barrel1.png
tankRed_barrel1.png
:resources:images/topdown_tanks/tileSand_roadSplitW.png
tileSand_roadSplitW.png
:resources:images/topdown_tanks/tileGrass_roadEast.png
tileGrass_roadEast.png
:resources:images/topdown_tanks/tankSand_barrel2.png
tankSand_barrel2.png
:resources:images/topdown_tanks/tankBody_red_outline.png
tankBody_red_outline.png
:resources:images/topdown_tanks/tileGrass_roadSplitS.png
tileGrass_roadSplitS.png
:resources:images/topdown_tanks/tileGrass_roadCornerLR.png
tileGrass_roadCornerLR.png
:resources:images/topdown_tanks/tileGrass_transitionE.png
tileGrass_transitionE.png
:resources:images/topdown_tanks/tankBody_blue.png
tankBody_blue.png
:resources:images/topdown_tanks/treeBrown_small.png
treeBrown_small.png
:resources:images/topdown_tanks/tankBlue_barrel2.png
tankBlue_barrel2.png
:resources:images/topdown_tanks/tankBody_darkLarge_outline.png
tankBody_darkLarge_outline.png
:resources:images/topdown_tanks/tank_green.png
tank_green.png
:resources:images/topdown_tanks/tileGrass_roadTransitionN.png
tileGrass_roadTransitionN.png
:resources:images/topdown_tanks/tileGrass_roadTransitionW.png
tileGrass_roadTransitionW.png
:resources:images/topdown_tanks/tileGrass_roadTransitionS_dirt.png
tileGrass_roadTransitionS_dirt.png
:resources:images/topdown_tanks/tankRed_barrel1_outline.png
tankRed_barrel1_outline.png
:resources:images/topdown_tanks/tankDark_barrel2_outline.png
tankDark_barrel2_outline.png
:resources:images/topdown_tanks/tankRed_barrel2.png
tankRed_barrel2.png
:resources:images/topdown_tanks/tileGrass_roadCrossingRound.png
tileGrass_roadCrossingRound.png
:resources:images/topdown_tanks/tileGrass_roadTransitionE.png
tileGrass_roadTransitionE.png
:resources:images/topdown_tanks/tileGrass_transitionS.png
tileGrass_transitionS.png
:resources:images/topdown_tanks/tank_sand.png
tank_sand.png
:resources:images/topdown_tanks/tankGreen_barrel1_outline.png
tankGreen_barrel1_outline.png
:resources:images/topdown_tanks/tankBody_blue_outline.png
tankBody_blue_outline.png
:resources:images/topdown_tanks/tankBlue_barrel3.png
tankBlue_barrel3.png
:resources:images/topdown_tanks/tileGrass1.png
tileGrass1.png
:resources:images/topdown_tanks/tracksDouble.png
tracksDouble.png
:resources:images/topdown_tanks/tileSand_roadCrossing.png
tileSand_roadCrossing.png
:resources:images/topdown_tanks/tileSand_roadCrossingRound.png
tileSand_roadCrossingRound.png
:resources:images/topdown_tanks/tankBlue_barrel1.png
tankBlue_barrel1.png
:resources:images/topdown_tanks/tileGrass_roadSplitW.png
tileGrass_roadSplitW.png
:resources:images/topdown_tanks/tileSand_roadCornerUL.png
tileSand_roadCornerUL.png
:resources:images/topdown_tanks/tileSand_roadSplitS.png
tileSand_roadSplitS.png
:resources:images/topdown_tanks/tankRed_barrel2_outline.png
tankRed_barrel2_outline.png
:resources:images/topdown_tanks/tankSand_barrel1.png
tankSand_barrel1.png
:resources:images/topdown_tanks/tankBlue_barrel1_outline.png
tankBlue_barrel1_outline.png
:resources:images/topdown_tanks/tankBody_bigRed_outline.png
tankBody_bigRed_outline.png
:resources:images/topdown_tanks/tileGrass2.png
tileGrass2.png
:resources:images/topdown_tanks/tankGreen_barrel3_outline.png
tankGreen_barrel3_outline.png
:resources:images/topdown_tanks/tileSand_roadSplitN.png
tileSand_roadSplitN.png
:resources:images/topdown_tanks/tankDark_barrel3.png
tankDark_barrel3.png
:resources:images/topdown_tanks/tileGrass_roadCornerUR.png
tileGrass_roadCornerUR.png
:resources:images/topdown_tanks/tankBody_sand_outline.png
tankBody_sand_outline.png
:resources:images/topdown_tanks/treeGreen_small.png
treeGreen_small.png
:resources:images/topdown_tanks/tileSand1.png
tileSand1.png
:resources:images/topdown_tanks/tank_dark.png
tank_dark.png
:resources:images/topdown_tanks/tankSand_barrel1_outline.png
tankSand_barrel1_outline.png
:resources:images/topdown_tanks/tileSand_roadCornerUR.png
tileSand_roadCornerUR.png
:resources:images/topdown_tanks/treeGreen_large.png
treeGreen_large.png
:resources:images/topdown_tanks/tankBody_dark_outline.png
tankBody_dark_outline.png
:resources:images/topdown_tanks/tankBody_red.png
tankBody_red.png
:resources:images/topdown_tanks/tankBody_dark.png
tankBody_dark.png
:resources:images/topdown_tanks/tankBody_sand.png
tankBody_sand.png
:resources:images/topdown_tanks/tileGrass_transitionW.png
tileGrass_transitionW.png
:resources:images/topdown_tanks/tileGrass_roadNorth.png
tileGrass_roadNorth.png
:resources:images/topdown_tanks/tileSand_roadEast.png
tileSand_roadEast.png
:resources:images/topdown_tanks/tileGrass_roadTransitionE_dirt.png
tileGrass_roadTransitionE_dirt.png
:resources:images/topdown_tanks/tankGreen_barrel3.png
tankGreen_barrel3.png
:resources:images/topdown_tanks/tankBlue_barrel2_outline.png
tankBlue_barrel2_outline.png
:resources:images/topdown_tanks/tankDark_barrel1_outline.png
tankDark_barrel1_outline.png
:resources:images/topdown_tanks/tileSand_roadNorth.png
tileSand_roadNorth.png
:resources:images/topdown_tanks/tileGrass_roadCornerLL.png
tileGrass_roadCornerLL.png
:resources:images/topdown_tanks/tankBody_darkLarge.png
tankBody_darkLarge.png
:resources:images/topdown_tanks/tankDark_barrel1.png
tankDark_barrel1.png
:resources:images/topdown_tanks/tankGreen_barrel2_outline.png
tankGreen_barrel2_outline.png
:resources:images/topdown_tanks/tankBody_green.png
tankBody_green.png
:resources:images/topdown_tanks/tankBody_bigRed.png
tankBody_bigRed.png
:resources:images/topdown_tanks/tileGrass_roadCornerUL.png
tileGrass_roadCornerUL.png
:resources:images/topdown_tanks/tileSand_roadSplitE.png
tileSand_roadSplitE.png
:resources:images/topdown_tanks/tankSand_barrel2_outline.png
tankSand_barrel2_outline.png
:resources:images/topdown_tanks/tankSand_barrel3.png
tankSand_barrel3.png
:resources:images/topdown_tanks/tracksSmall.png
tracksSmall.png
:resources:images/topdown_tanks/tankRed_barrel3_outline.png
tankRed_barrel3_outline.png
:resources:images/topdown_tanks/tankBlue_barrel3_outline.png
tankBlue_barrel3_outline.png
:resources:images/topdown_tanks/tankDark_barrel2.png
tankDark_barrel2.png
:resources:images/topdown_tanks/tank_blue.png
tank_blue.png
:resources:images/topdown_tanks/tileGrass_roadTransitionS.png
tileGrass_roadTransitionS.png
:resources:images/topdown_tanks/tileGrass_roadSplitE.png
tileGrass_roadSplitE.png
:resources:images/topdown_tanks/tankSand_barrel3_outline.png
tankSand_barrel3_outline.png
:resources:images/topdown_tanks/tileGrass_roadTransitionN_dirt.png
tileGrass_roadTransitionN_dirt.png
:resources:images/topdown_tanks/tankBody_huge_outline.png
tankBody_huge_outline.png
:resources:images/topdown_tanks/tankBody_huge.png
tankBody_huge.png
:resources:images/topdown_tanks/tankBody_green_outline.png
tankBody_green_outline.png
:resources:images/topdown_tanks/tileSand_roadCornerLR.png
tileSand_roadCornerLR.png
:resources:images/topdown_tanks/tankGreen_barrel2.png
tankGreen_barrel2.png
:resources:images/topdown_tanks/tileSand2.png
tileSand2.png
:resources:images/topdown_tanks/tileGrass_roadSplitN.png
tileGrass_roadSplitN.png
:resources:images/animated_characters/male_person/
:resources:images/animated_characters/male_person/malePerson_walk4.png
malePerson_walk4.png
:resources:images/animated_characters/male_person/malePerson_walk7.png
malePerson_walk7.png
:resources:images/animated_characters/male_person/malePerson_walk2.png
malePerson_walk2.png
:resources:images/animated_characters/male_person/malePerson_idle.png
malePerson_idle.png
:resources:images/animated_characters/male_person/malePerson_walk6.png
malePerson_walk6.png
:resources:images/animated_characters/male_person/malePerson_walk0.png
malePerson_walk0.png
:resources:images/animated_characters/male_person/malePerson_walk5.png
malePerson_walk5.png
:resources:images/animated_characters/male_person/malePerson_climb0.png
malePerson_climb0.png
:resources:images/animated_characters/male_person/malePerson_walk3.png
malePerson_walk3.png
:resources:images/animated_characters/male_person/malePerson_walk1.png
malePerson_walk1.png
:resources:images/animated_characters/male_person/malePerson_fall.png
malePerson_fall.png
:resources:images/animated_characters/male_person/malePerson_climb1.png
malePerson_climb1.png
:resources:images/animated_characters/male_person/malePerson_jump.png
malePerson_jump.png
:resources:images/animated_characters/female_person/
:resources:images/animated_characters/female_person/femalePerson_walk2.png
femalePerson_walk2.png
:resources:images/animated_characters/female_person/femalePerson_fall.png
femalePerson_fall.png
:resources:images/animated_characters/female_person/femalePerson_walk4.png
femalePerson_walk4.png
:resources:images/animated_characters/female_person/femalePerson_climb1.png
femalePerson_climb1.png
:resources:images/animated_characters/female_person/femalePerson_walk3.png
femalePerson_walk3.png
:resources:images/animated_characters/female_person/femalePerson_walk1.png
femalePerson_walk1.png
:resources:images/animated_characters/female_person/femalePerson_climb0.png
femalePerson_climb0.png
:resources:images/animated_characters/female_person/femalePerson_walk0.png
femalePerson_walk0.png
:resources:images/animated_characters/female_person/femalePerson_walk7.png
femalePerson_walk7.png
:resources:images/animated_characters/female_person/femalePerson_jump.png
femalePerson_jump.png
:resources:images/animated_characters/female_person/femalePerson_idle.png
femalePerson_idle.png
:resources:images/animated_characters/female_person/femalePerson_walk5.png
femalePerson_walk5.png
:resources:images/animated_characters/female_person/femalePerson_walk6.png
femalePerson_walk6.png
:resources:images/animated_characters/zombie/
:resources:images/animated_characters/zombie/zombie_walk1.png
zombie_walk1.png
:resources:images/animated_characters/zombie/zombie_walk3.png
zombie_walk3.png
:resources:images/animated_characters/zombie/zombie_walk2.png
zombie_walk2.png
:resources:images/animated_characters/zombie/zombie_walk4.png
zombie_walk4.png
:resources:images/animated_characters/zombie/zombie_climb0.png
zombie_climb0.png
:resources:images/animated_characters/zombie/zombie_fall.png
zombie_fall.png
:resources:images/animated_characters/zombie/zombie_walk5.png
zombie_walk5.png
:resources:images/animated_characters/zombie/zombie_climb1.png
zombie_climb1.png
:resources:images/animated_characters/zombie/zombie_idle.png
zombie_idle.png
:resources:images/animated_characters/zombie/zombie_walk6.png
zombie_walk6.png
:resources:images/animated_characters/zombie/zombie_walk7.png
zombie_walk7.png
:resources:images/animated_characters/zombie/zombie_walk0.png
zombie_walk0.png
:resources:images/animated_characters/zombie/zombie_jump.png
zombie_jump.png
:resources:images/animated_characters/female_adventurer/
:resources:images/animated_characters/female_adventurer/femaleAdventurer_walk5.png
femaleAdventurer_walk5.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_climb0.png
femaleAdventurer_climb0.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_walk2.png
femaleAdventurer_walk2.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_climb1.png
femaleAdventurer_climb1.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_walk1.png
femaleAdventurer_walk1.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_fall.png
femaleAdventurer_fall.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_jump.png
femaleAdventurer_jump.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_walk6.png
femaleAdventurer_walk6.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_walk0.png
femaleAdventurer_walk0.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_walk7.png
femaleAdventurer_walk7.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_walk3.png
femaleAdventurer_walk3.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png
femaleAdventurer_idle.png
:resources:images/animated_characters/female_adventurer/femaleAdventurer_walk4.png
femaleAdventurer_walk4.png
:resources:images/animated_characters/male_adventurer/
:resources:images/animated_characters/male_adventurer/maleAdventurer_walk0.png
maleAdventurer_walk0.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_idle.png
maleAdventurer_idle.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_walk1.png
maleAdventurer_walk1.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_walk4.png
maleAdventurer_walk4.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_climb0.png
maleAdventurer_climb0.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_fall.png
maleAdventurer_fall.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_walk7.png
maleAdventurer_walk7.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_walk3.png
maleAdventurer_walk3.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_walk2.png
maleAdventurer_walk2.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_climb1.png
maleAdventurer_climb1.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_jump.png
maleAdventurer_jump.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_walk5.png
maleAdventurer_walk5.png
:resources:images/animated_characters/male_adventurer/maleAdventurer_walk6.png
maleAdventurer_walk6.png
:resources:images/animated_characters/robot/
:resources:images/animated_characters/robot/robot_fall.png
robot_fall.png
:resources:images/animated_characters/robot/robot_walk7.png
robot_walk7.png
:resources:images/animated_characters/robot/robot_walk0.png
robot_walk0.png
:resources:images/animated_characters/robot/robot_walk1.png
robot_walk1.png
:resources:images/animated_characters/robot/robot_idle.png
robot_idle.png
:resources:images/animated_characters/robot/robot_walk2.png
robot_walk2.png
:resources:images/animated_characters/robot/robot_walk5.png
robot_walk5.png
:resources:images/animated_characters/robot/robot_walk4.png
robot_walk4.png
:resources:images/animated_characters/robot/robot_walk3.png
robot_walk3.png
:resources:images/animated_characters/robot/robot_climb1.png
robot_climb1.png
:resources:images/animated_characters/robot/robot_jump.png
robot_jump.png
:resources:images/animated_characters/robot/robot_walk6.png
robot_walk6.png
:resources:images/animated_characters/robot/robot_climb0.png
robot_climb0.png
:resources:images/cards/
:resources:images/cards/cardSpades10.png
cardSpades10.png
:resources:images/cards/cardHearts7.png
cardHearts7.png
:resources:images/cards/cardSpadesK.png
cardSpadesK.png
:resources:images/cards/cardClubsQ.png
cardClubsQ.png
:resources:images/cards/cardClubs4.png
cardClubs4.png
:resources:images/cards/cardClubs9.png
cardClubs9.png
:resources:images/cards/cardDiamonds4.png
cardDiamonds4.png
:resources:images/cards/cardBack_blue5.png
cardBack_blue5.png
:resources:images/cards/cardDiamonds6.png
cardDiamonds6.png
:resources:images/cards/cardSpadesA.png
cardSpadesA.png
:resources:images/cards/cardHeartsJ.png
cardHeartsJ.png
:resources:images/cards/cardHearts5.png
cardHearts5.png
:resources:images/cards/cardSpadesQ.png
cardSpadesQ.png
:resources:images/cards/cardHeartsA.png
cardHeartsA.png
:resources:images/cards/cardClubs5.png
cardClubs5.png
:resources:images/cards/cardHearts8.png
cardHearts8.png
:resources:images/cards/cardBack_blue2.png
cardBack_blue2.png
:resources:images/cards/cardHeartsQ.png
cardHeartsQ.png
:resources:images/cards/cardJoker.png
cardJoker.png
:resources:images/cards/cardDiamonds5.png
cardDiamonds5.png
:resources:images/cards/cardDiamonds10.png
cardDiamonds10.png
:resources:images/cards/cardClubs3.png
cardClubs3.png
:resources:images/cards/cardSpades6.png
cardSpades6.png
:resources:images/cards/cardClubs10.png
cardClubs10.png
:resources:images/cards/cardSpades3.png
cardSpades3.png
:resources:images/cards/cardDiamondsA.png
cardDiamondsA.png
:resources:images/cards/cardBack_red5.png
cardBack_red5.png
:resources:images/cards/cardDiamondsK.png
cardDiamondsK.png
:resources:images/cards/cardDiamonds8.png
cardDiamonds8.png
:resources:images/cards/cardBack_red2.png
cardBack_red2.png
:resources:images/cards/cardSpades7.png
cardSpades7.png
:resources:images/cards/cardClubs7.png
cardClubs7.png
:resources:images/cards/cardBack_blue1.png
cardBack_blue1.png
:resources:images/cards/cardDiamondsJ.png
cardDiamondsJ.png
:resources:images/cards/cardClubsA.png
cardClubsA.png
:resources:images/cards/cardDiamonds3.png
cardDiamonds3.png
:resources:images/cards/cardBack_red4.png
cardBack_red4.png
:resources:images/cards/cardHearts2.png
cardHearts2.png
:resources:images/cards/cardBack_green3.png
cardBack_green3.png
:resources:images/cards/cardDiamonds2.png
cardDiamonds2.png
:resources:images/cards/cardDiamonds9.png
cardDiamonds9.png
:resources:images/cards/cardClubsK.png
cardClubsK.png
:resources:images/cards/cardClubs2.png
cardClubs2.png
:resources:images/cards/cardBack_green2.png
cardBack_green2.png
:resources:images/cards/cardBack_blue3.png
cardBack_blue3.png
:resources:images/cards/cardDiamonds7.png
cardDiamonds7.png
:resources:images/cards/cardSpades4.png
cardSpades4.png
:resources:images/cards/cardHearts3.png
cardHearts3.png
:resources:images/cards/cardBack_red3.png
cardBack_red3.png
:resources:images/cards/cardBack_green1.png
cardBack_green1.png
:resources:images/cards/cardHearts6.png
cardHearts6.png
:resources:images/cards/cardSpades2.png
cardSpades2.png
:resources:images/cards/cardHearts4.png
cardHearts4.png
:resources:images/cards/cardDiamondsQ.png
cardDiamondsQ.png
:resources:images/cards/cardBack_red1.png
cardBack_red1.png
:resources:images/cards/cardBack_green4.png
cardBack_green4.png
:resources:images/cards/cardBack_green5.png
cardBack_green5.png
:resources:images/cards/cardHearts9.png
cardHearts9.png
:resources:images/cards/cardSpades8.png
cardSpades8.png
:resources:images/cards/cardSpades5.png
cardSpades5.png
:resources:images/cards/cardBack_blue4.png
cardBack_blue4.png
:resources:images/cards/cardClubs8.png
cardClubs8.png
:resources:images/cards/cardHeartsK.png
cardHeartsK.png
:resources:images/cards/cardSpadesJ.png
cardSpadesJ.png
:resources:images/cards/cardHearts10.png
cardHearts10.png
:resources:images/cards/cardSpades9.png
cardSpades9.png
:resources:images/cards/cardClubsJ.png
cardClubsJ.png
:resources:images/cards/cardClubs6.png
cardClubs6.png
:resources:images/backgrounds/
:resources:images/backgrounds/abstract_2.jpg
abstract_2.jpg
:resources:images/backgrounds/instructions_0.png
instructions_0.png
:resources:images/backgrounds/instructions_1.png
instructions_1.png
:resources:images/backgrounds/abstract_1.jpg
abstract_1.jpg
:resources:images/backgrounds/stars.png
stars.png
:resources:images/pinball/
:resources:images/pinball/pool_cue_ball.png
pool_cue_ball.png
:resources:images/pinball/bumper.png
bumper.png
:resources:images/space_shooter/
:resources:images/space_shooter/meteorGrey_big4.png
meteorGrey_big4.png
:resources:images/space_shooter/laserBlue01.png
laserBlue01.png
:resources:images/space_shooter/meteorGrey_tiny1.png
meteorGrey_tiny1.png
:resources:images/space_shooter/playerLife1_orange.png
playerLife1_orange.png
:resources:images/space_shooter/meteorGrey_big3.png
meteorGrey_big3.png
:resources:images/space_shooter/meteorGrey_med2.png
meteorGrey_med2.png
:resources:images/space_shooter/meteorGrey_big2.png
meteorGrey_big2.png
:resources:images/space_shooter/meteorGrey_big1.png
meteorGrey_big1.png
:resources:images/space_shooter/playerShip2_orange.png
playerShip2_orange.png
:resources:images/space_shooter/laserRed01.png
laserRed01.png
:resources:images/space_shooter/meteorGrey_small2.png
meteorGrey_small2.png
:resources:images/space_shooter/playerShip3_orange.png
playerShip3_orange.png
:resources:images/space_shooter/meteorGrey_small1.png
meteorGrey_small1.png
:resources:images/space_shooter/meteorGrey_tiny2.png
meteorGrey_tiny2.png
:resources:images/space_shooter/meteorGrey_med1.png
meteorGrey_med1.png
:resources:images/space_shooter/playerShip1_orange.png
playerShip1_orange.png
:resources:images/space_shooter/playerShip1_green.png
playerShip1_green.png
:resources:images/enemies/
:resources:images/enemies/ladybug.png
ladybug.png
:resources:images/enemies/frog.png
frog.png
:resources:images/enemies/sawHalf.png
sawHalf.png
:resources:images/enemies/wormPink.png
wormPink.png
:resources:images/enemies/slimeBlue_move.png
slimeBlue_move.png
:resources:images/enemies/mouse.png
mouse.png
:resources:images/enemies/bee.png
bee.png
:resources:images/enemies/slimeGreen.png
slimeGreen.png
:resources:images/enemies/fishGreen.png
fishGreen.png
:resources:images/enemies/wormGreen.png
wormGreen.png
:resources:images/enemies/slimeBlock.png
slimeBlock.png
:resources:images/enemies/fly.png
fly.png
:resources:images/enemies/wormGreen_dead.png
wormGreen_dead.png
:resources:images/enemies/saw.png
saw.png
:resources:images/enemies/slimePurple.png
slimePurple.png
:resources:images/enemies/wormGreen_move.png
wormGreen_move.png
:resources:images/enemies/slimeBlue.png
slimeBlue.png
:resources:images/enemies/frog_move.png
frog_move.png
:resources:images/enemies/fishPink.png
fishPink.png
:resources:images/items/
:resources:images/items/coinGold_ul.png
coinGold_ul.png
:resources:images/items/keyYellow.png
keyYellow.png
:resources:images/items/gold_4.png
gold_4.png
:resources:images/items/flagGreen_down.png
flagGreen_down.png
:resources:images/items/coinSilver.png
coinSilver.png
:resources:images/items/gold_2.png
gold_2.png
:resources:images/items/ladderTop.png
ladderTop.png
:resources:images/items/flagRed_down.png
flagRed_down.png
:resources:images/items/gemYellow.png
gemYellow.png
:resources:images/items/star.png
star.png
:resources:images/items/coinBronze.png
coinBronze.png
:resources:images/items/coinGold.png
coinGold.png
:resources:images/items/flagRed1.png
flagRed1.png
:resources:images/items/keyGreen.png
keyGreen.png
:resources:images/items/flagGreen1.png
flagGreen1.png
:resources:images/items/flagYellow1.png
flagYellow1.png
:resources:images/items/gemBlue.png
gemBlue.png
:resources:images/items/gold_3.png
gold_3.png
:resources:images/items/flagRed2.png
flagRed2.png
:resources:images/items/flagYellow_down.png
flagYellow_down.png
:resources:images/items/gold_1.png
gold_1.png
:resources:images/items/coinGold_ur.png
coinGold_ur.png
:resources:images/items/coinGold_lr.png
coinGold_lr.png
:resources:images/items/gemGreen.png
gemGreen.png
:resources:images/items/flagYellow2.png
flagYellow2.png
:resources:images/items/keyRed.png
keyRed.png
:resources:images/items/gemRed.png
gemRed.png
:resources:images/items/ladderMid.png
ladderMid.png
:resources:images/items/flagGreen2.png
flagGreen2.png
:resources:images/items/keyBlue.png
keyBlue.png
:resources:images/items/coinSilver_test.png
coinSilver_test.png
:resources:images/items/coinGold_ll.png
coinGold_ll.png
:resources:images/alien/
:resources:images/alien/alienBlue_jump.png
alienBlue_jump.png
:resources:images/alien/alienBlue_climb1.png
alienBlue_climb1.png
:resources:images/alien/alienBlue_walk2.png
alienBlue_walk2.png
:resources:images/alien/alienBlue_climb2.png
alienBlue_climb2.png
:resources:images/alien/alienBlue_walk1.png
alienBlue_walk1.png
:resources:images/alien/alienBlue_front.png
alienBlue_front.png
:resources:images/test_textures/
:resources:images/test_textures/xy_square.png
xy_square.png
:resources:images/test_textures/test_texture.png
test_texture.png
:resources:images/tiles/
:resources:images/tiles/sandHalf_left.png
sandHalf_left.png
:resources:images/tiles/sandCorner_right.png
sandCorner_right.png
:resources:images/tiles/grassCorner_left.png
grassCorner_left.png
:resources:images/tiles/snowLeft.png
snowLeft.png
:resources:images/tiles/brickGrey.png
brickGrey.png
:resources:images/tiles/lockYellow.png
lockYellow.png
:resources:images/tiles/stoneHalf.png
stoneHalf.png
:resources:images/tiles/stoneHalf_right.png
stoneHalf_right.png
:resources:images/tiles/grassCorner_right.png
grassCorner_right.png
:resources:images/tiles/planetHill_left.png
planetHill_left.png
:resources:images/tiles/grassMid.png
grassMid.png
:resources:images/tiles/waterTop_high.png
waterTop_high.png
:resources:images/tiles/stoneLeft.png
stoneLeft.png
:resources:images/tiles/sandHalf_mid.png
sandHalf_mid.png
:resources:images/tiles/stoneCliffAlt_left.png
stoneCliffAlt_left.png
:resources:images/tiles/brickBrown.png
brickBrown.png
:resources:images/tiles/grass.png
grass.png
:resources:images/tiles/snowMid.png
snowMid.png
:resources:images/tiles/sandHalf.png
sandHalf.png
:resources:images/tiles/sandCliff_right.png
sandCliff_right.png
:resources:images/tiles/water.png
water.png
:resources:images/tiles/grassHalf_mid.png
grassHalf_mid.png
:resources:images/tiles/dirtHalf.png
dirtHalf.png
:resources:images/tiles/ladderTop.png
ladderTop.png
:resources:images/tiles/signRight.png
signRight.png
:resources:images/tiles/planetLeft.png
planetLeft.png
:resources:images/tiles/dirtCorner_left.png
dirtCorner_left.png
:resources:images/tiles/planetCorner_left.png
planetCorner_left.png
:resources:images/tiles/sandHill_left.png
sandHill_left.png
:resources:images/tiles/rock.png
rock.png
:resources:images/tiles/grassCliff_right.png
grassCliff_right.png
:resources:images/tiles/stoneHalf_left.png
stoneHalf_left.png
:resources:images/tiles/lavaTop_low.png
lavaTop_low.png
:resources:images/tiles/planetCenter.png
planetCenter.png
:resources:images/tiles/planetMid.png
planetMid.png
:resources:images/tiles/grassHalf_right.png
grassHalf_right.png
:resources:images/tiles/sandHill_right.png
sandHill_right.png
:resources:images/tiles/sandRight.png
sandRight.png
:resources:images/tiles/switchGreen.png
switchGreen.png
:resources:images/tiles/dirtHalf_mid.png
dirtHalf_mid.png
:resources:images/tiles/brickTextureWhite.png
brickTextureWhite.png
:resources:images/tiles/stoneCorner_right.png
stoneCorner_right.png
:resources:images/tiles/dirtCenter.png
dirtCenter.png
:resources:images/tiles/stoneHill_right.png
stoneHill_right.png
:resources:images/tiles/snowCenter.png
snowCenter.png
:resources:images/tiles/dirtHalf_right.png
dirtHalf_right.png
:resources:images/tiles/snowHalf_left.png
snowHalf_left.png
:resources:images/tiles/planetCliffAlt_right.png
planetCliffAlt_right.png
:resources:images/tiles/dirt.png
dirt.png
:resources:images/tiles/snowCliff_right.png
snowCliff_right.png
:resources:images/tiles/doorClosed_top.png
doorClosed_top.png
:resources:images/tiles/snowCorner_left.png
snowCorner_left.png
:resources:images/tiles/signLeft.png
signLeft.png
:resources:images/tiles/mushroomRed.png
mushroomRed.png
:resources:images/tiles/planetHill_right.png
planetHill_right.png
:resources:images/tiles/dirtCorner_right.png
dirtCorner_right.png
:resources:images/tiles/snowHalf.png
snowHalf.png
:resources:images/tiles/boxCrate.png
boxCrate.png
:resources:images/tiles/cactus.png
cactus.png
:resources:images/tiles/grassHalf.png
grassHalf.png
:resources:images/tiles/grassRight.png
grassRight.png
:resources:images/tiles/leverMid.png
leverMid.png
:resources:images/tiles/stoneCorner_left.png
stoneCorner_left.png
:resources:images/tiles/snowCorner_right.png
snowCorner_right.png
:resources:images/tiles/grassHalf_left.png
grassHalf_left.png
:resources:images/tiles/snowRight.png
snowRight.png
:resources:images/tiles/sandMid.png
sandMid.png
:resources:images/tiles/stoneRight.png
stoneRight.png
:resources:images/tiles/bridgeA.png
bridgeA.png
:resources:images/tiles/waterTop_low.png
waterTop_low.png
:resources:images/tiles/stone.png
stone.png
:resources:images/tiles/snowHill_left.png
snowHill_left.png
:resources:images/tiles/planetCenter_rounded.png
planetCenter_rounded.png
:resources:images/tiles/planetCliffAlt_left.png
planetCliffAlt_left.png
:resources:images/tiles/dirtRight.png
dirtRight.png
:resources:images/tiles/grassCliff_left.png
grassCliff_left.png
:resources:images/tiles/switchRed.png
switchRed.png
:resources:images/tiles/stoneHalf_mid.png
stoneHalf_mid.png
:resources:images/tiles/dirtCliffAlt_right.png
dirtCliffAlt_right.png
:resources:images/tiles/lavaTop_high.png
lavaTop_high.png
:resources:images/tiles/signExit.png
signExit.png
:resources:images/tiles/bomb.png
bomb.png
:resources:images/tiles/grassHill_right.png
grassHill_right.png
:resources:images/tiles/dirtHill_right.png
dirtHill_right.png
:resources:images/tiles/snowCenter_rounded.png
snowCenter_rounded.png
:resources:images/tiles/sandCliffAlt_left.png
sandCliffAlt_left.png
:resources:images/tiles/dirtCenter_rounded.png
dirtCenter_rounded.png
:resources:images/tiles/leverLeft.png
leverLeft.png
:resources:images/tiles/stoneCliffAlt_right.png
stoneCliffAlt_right.png
:resources:images/tiles/switchGreen_pressed.png
switchGreen_pressed.png
:resources:images/tiles/grassHill_left.png
grassHill_left.png
:resources:images/tiles/sandCenter.png
sandCenter.png
:resources:images/tiles/stoneCenter.png
stoneCenter.png
:resources:images/tiles/grassLeft.png
grassLeft.png
:resources:images/tiles/bridgeB.png
bridgeB.png
:resources:images/tiles/planetCliff_left.png
planetCliff_left.png
:resources:images/tiles/planetCliff_right.png
planetCliff_right.png
:resources:images/tiles/torch1.png
torch1.png
:resources:images/tiles/boxCrate_single.png
boxCrate_single.png
:resources:images/tiles/lockRed.png
lockRed.png
:resources:images/tiles/snowHalf_mid.png
snowHalf_mid.png
:resources:images/tiles/snowCliffAlt_left.png
snowCliffAlt_left.png
:resources:images/tiles/plantPurple.png
plantPurple.png
:resources:images/tiles/stoneCliff_right.png
stoneCliff_right.png
:resources:images/tiles/snowCliffAlt_right.png
snowCliffAlt_right.png
:resources:images/tiles/stoneCliff_left.png
stoneCliff_left.png
:resources:images/tiles/planetHalf_mid.png
planetHalf_mid.png
:resources:images/tiles/grass_sprout.png
grass_sprout.png
:resources:images/tiles/grassCliffAlt_right.png
grassCliffAlt_right.png
:resources:images/tiles/stoneHill_left.png
stoneHill_left.png
:resources:images/tiles/snow.png
snow.png
:resources:images/tiles/dirtCliff_left.png
dirtCliff_left.png
:resources:images/tiles/bush.png
bush.png
:resources:images/tiles/doorClosed_mid.png
doorClosed_mid.png
:resources:images/tiles/grassCliffAlt_left.png
grassCliffAlt_left.png
:resources:images/tiles/sandCenter_rounded.png
sandCenter_rounded.png
:resources:images/tiles/planet.png
planet.png
:resources:images/tiles/stoneCenter_rounded.png
stoneCenter_rounded.png
:resources:images/tiles/dirtMid.png
dirtMid.png
:resources:images/tiles/torch2.png
torch2.png
:resources:images/tiles/planetRight.png
planetRight.png
:resources:images/tiles/dirtCliffAlt_left.png
dirtCliffAlt_left.png
:resources:images/tiles/spikes.png
spikes.png
:resources:images/tiles/sandCliffAlt_right.png
sandCliffAlt_right.png
:resources:images/tiles/snowHill_right.png
snowHill_right.png
:resources:images/tiles/dirtHill_left.png
dirtHill_left.png
:resources:images/tiles/planetHalf_right.png
planetHalf_right.png
:resources:images/tiles/ladderMid.png
ladderMid.png
:resources:images/tiles/snowCliff_left.png
snowCliff_left.png
:resources:images/tiles/snowHalf_right.png
snowHalf_right.png
:resources:images/tiles/snow_pile.png
snow_pile.png
:resources:images/tiles/dirtLeft.png
dirtLeft.png
:resources:images/tiles/planetCorner_right.png
planetCorner_right.png
:resources:images/tiles/planetHalf_left.png
planetHalf_left.png
:resources:images/tiles/torchOff.png
torchOff.png
:resources:images/tiles/boxCrate_double.png
boxCrate_double.png
:resources:images/tiles/leverRight.png
leverRight.png
:resources:images/tiles/stoneMid.png
stoneMid.png
:resources:images/tiles/switchRed_pressed.png
switchRed_pressed.png
:resources:images/tiles/dirtCliff_right.png
dirtCliff_right.png
:resources:images/tiles/sandLeft.png
sandLeft.png
:resources:images/tiles/grassCenter_round.png
grassCenter_round.png
:resources:images/tiles/dirtHalf_left.png
dirtHalf_left.png
:resources:images/tiles/sandCorner_left.png
sandCorner_left.png
:resources:images/tiles/planetHalf.png
planetHalf.png
:resources:images/tiles/sandHalf_right.png
sandHalf_right.png
:resources:images/tiles/grassCenter.png
grassCenter.png
:resources:images/tiles/sandCliff_left.png
sandCliff_left.png
:resources:images/tiles/lava.png
lava.png
:resources:images/tiles/sand.png
sand.png
:resources:images/spritesheets/
:resources:images/spritesheets/number_sheet.png
number_sheet.png
:resources:images/spritesheets/tiles.png
tiles.png
:resources:images/spritesheets/codepage_437.png
codepage_437.png
:resources:images/spritesheets/explosion.png
explosion.png
:resources:music/

1918.mp3

funkyrobot.mp3
:resources:style/
default.yml
:resources:tmx_maps/
level_2.tmx level_1.tmx test_objects.tmx
items.tsx map_with_custom_hitboxes.tmx more_tiles.tsx
standard_tileset.tsx map2_level_2.tmx dirt.tsx
map_with_external_tileset.tmx test_map_6.tmx spritesheet.tsx
test_map_3.tmx map_with_ladders.tmx isometric_dungeon.tmx
test_map_1.tmx map2_level_1.tmx test_map_5.tmx
map.tmx grass.tsx test_map_4.tmx
test_map_2.tmx test_map_7.tmx
:resources:shaders/
shape_element_list_fs.glsl texture_default_projection_vs.glsl shape_element_list_vs.glsl
texture_fs.glsl __init__.py
:resources:shaders/shapes/
README.md
:resources:shaders/shapes/line/
unbuffered_fs.glsl line_generic_with_colors_vs.glsl line_vertex_shader_fs.glsl
unbuffered_geo.glsl buffered_geo.glsl unbuffered_vs.glsl
line_generic_with_colors_fs.glsl line_vertex_shader_vs.glsl buffered_fs.glsl
buffered_vs.glsl
:resources:shaders/shapes/rectangle/
filled_unbuffered_vs.glsl filled_unbuffered_fs.glsl filled_unbuffered_geo.glsl
:resources:shaders/shapes/ellipse/
outline_unbuffered_fs.glsl filled_unbuffered_vs.glsl filled_unbuffered_fs.glsl
filled_unbuffered_geo.glsl outline_unbuffered_geo.glsl outline_unbuffered_vs.glsl
:resources:shaders/lights/
combine_vs.glsl point_lights_vs.glsl point_lights_fs.glsl
combine_fs.glsl point_lights_geo.glsl
:resources:shaders/postprocessing/
gaussian_blur_y_fs.glsl gaussian_combine_fs.glsl glow_filter_vs.glsl
glow_filter_fs.glsl gaussian_blur_x_fs.glsl
:resources:shaders/sprites/
sprite_list_instanced_fs.glsl sprite_list_geometry_cull_geo.glsl sprite_list_instanced_vs.glsl
sprite_list_geometry_no_cull_geo.glsl sprite_list_geometry_fs.glsl sprite_list_geometry_vs.glsl
:resources:sounds/

explosion2.wav

hurt5.wav

error4.wav

fall2.wav

hurt2.wav

laser4.wav

gameover3.wav

upgrade4.wav

coin5.wav

jump2.wav

rockHit2.ogg

laser5.wav

error1.wav

jump3.wav

laser1.wav

hurt4.wav

error2.wav

upgrade1.wav

secret4.wav

gameover4.wav

upgrade5.wav

jump4.wav

hurt3.wav

lose4.wav

hit3.wav

lose3.wav

laser1.mp3

hit4.wav

gameover5.wav

fall1.wav

phaseJump1.wav

fall3.wav

hurt1.wav

fall4.wav

error5.wav

rockHit2.wav

gameover1.wav

hit1.wav

error3.wav

coin4.wav

jump5.wav

jump1.wav

lose5.wav

laser3.wav

coin1.wav

coin3.wav

lose1.wav

upgrade3.wav

hit5.wav

upgrade2.wav

gameover2.wav

coin2.wav

explosion1.wav

hit2.wav

phaseJump1.ogg

secret2.wav

laser1.ogg

lose2.wav

laser2.wav
:resources:gui_basic_assets/
:resources:gui_basic_assets/red_button_normal.png
red_button_normal.png
:resources:gui_basic_assets/red_button_press.png
red_button_press.png
:resources:gui_basic_assets/red_button_hover.png
red_button_hover.png
:resources:gui_basic_assets/icons/
:resources:gui_basic_assets/icons/larger.png
larger.png
:resources:gui_basic_assets/icons/smaller.png
smaller.png

Main Arcade Module Package API

Working with the Keyboard

Modifiers

The modifiers that are held down when the event is generated are combined in a bitwise fashion and provided in the modifiers parameter. The modifier constants defined in arcade.key are:

MOD_SHIFT
MOD_CTRL
MOD_ALT         Not available on Mac OS X
MOD_WINDOWS     Available on Windows only
MOD_COMMAND     Available on Mac OS X only
MOD_OPTION      Available on Mac OS X only
MOD_CAPSLOCK
MOD_NUMLOCK
MOD_SCROLLLOCK
MOD_ACCEL       Equivalent to MOD_CTRL, or MOD_COMMAND on Mac OS X.

For example, to test if the shift key is held down:

if modifiers & MOD_SHIFT:
    pass

Unlike the corresponding key symbols, it is not possible to determine whether the left or right modifier is held down (though you could emulate this behavior by keeping track of the key states yourself).

Simple Platformer

_images/intro_screen.png

Use Python and the Arcade library to create a 2D platformer game. Learn to work with Sprites and the Tiled Map Editor to create your own games. Add coins, ramps, moving platforms, enemies, and more.

The tutorial is divided into these parts:

Step 1 - Install and Open a Window

Our first step is to make sure everything is installed, and that we can at least get a window open.

Installation
_images/file_structure.png
  • Make sure the Arcade library is installed.

    • Install Arcade with pip install arcade on Windows or pip3 install arcade on Mac/Linux. Or install by using a venv.

    • Here are the longer, official Installation Instructions.

I highly recommend using the free community edition of PyCharm as an editor. If you do, see Install Arcade with PyCharm and a Virtual Environment.

Open a Window

The example below opens up a blank window. Set up a project and get the code below working. (It is also in the zip file as 01_open_window.py.)

Note

This is a fixed-size window. It is possible to have a Resizable Window or a Full Screen Example, but there are more interesting things we can do first. Therefore we’ll stick with a fixed-size window for this tutorial.

01_open_window.py - Open a Window
 1"""
 2Platformer Game
 3"""
 4import arcade
 5
 6# Constants
 7SCREEN_WIDTH = 1000
 8SCREEN_HEIGHT = 650
 9SCREEN_TITLE = "Platformer"
10
11
12class MyGame(arcade.Window):
13    """
14    Main application class.
15    """
16
17    def __init__(self):
18
19        # Call the parent class and set up the window
20        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
21
22        arcade.set_background_color(arcade.csscolor.CORNFLOWER_BLUE)
23
24    def setup(self):
25        """ Set up the game here. Call this function to restart the game. """
26        pass
27
28    def on_draw(self):
29        """ Render the screen. """
30
31        arcade.start_render()
32        # Code to draw the screen goes here
33
34
35def main():
36    """ Main method """
37    window = MyGame()
38    window.setup()
39    arcade.run()
40
41
42if __name__ == "__main__":
43    main()

Once you get the code working, figure out how to:

  • Change the screen size

  • Change the title

  • Change the background color

  • Look through the documentation for the Window class to get an idea of everything it can do.

Step 2 - Add Sprites

Our next step is to add some sprites, which are graphics we can see and interact with on the screen.

_images/listing_02.png
Setup vs. Init

In the next code example, 02_draw_sprites, we’ll have both an __init__ method and a setup.

The __init__ creates the variables. The variables are set to values such as 0 or None. The setup actually creates the object instances, such as graphical sprites.

I often get the very reasonable question, “Why have two methods? Why not just put everything into __init__? Seems like we are doing twice the work.” Here’s why. With a setup method split out, later on we can easily add “restart/play again” functionality to the game. A simple call to setup will reset everything. Later, we can expand our game with different levels, and have functions such as setup_level_1 and setup_level_2.

Sprite Lists

Sprites are managed in lists. The SpriteList class optimizes drawing, movement, and collision detection.

We are using three logical groups in our game. A player_list for the player. A wall_list for walls we can’t move through. And finally a coin_list for coins we can pick up.

self.player_list = arcade.SpriteList()
self.wall_list = arcade.SpriteList(use_spatial_hash=True)
self.coin_list = arcade.SpriteList(use_spatial_hash=True)

Sprite lists have an option to use something called “spatial hashing.” Spatial hashing speeds the time it takes to find collisions, but increases the time it takes to move a sprite. Since I don’t expect most of my walls or coins to move, I’ll turn on spatial hashing for these lists. My player moves around a lot, so I’ll leave it off for her.

Add Sprites to the Game

To create sprites we’ll use the arcade.Sprite class. We can create an instance of the sprite class with code like this:

self.player_sprite = arcade.Sprite("images/player_1/player_stand.png", CHARACTER_SCALING)

The first parameter is a string or path to the image you want it to load. An optional second parameter will scale the sprite up or down. If the second parameter (in this case a constant CHARACTER_SCALING) is set to 0.5, and the the sprite is 128x128, then both width and height will be scaled down 50% for a 64x64 sprite.

Next, we need to tell where the sprite goes. You can use the attributes center_x and center_y to position the sprite. You can also use top, bottom, left, and right to get or set the sprites location by an edge instead of the center. You can also use position attribute to set both the x and y at the same time.

self.player_sprite.center_x = 64
self.player_sprite.center_y = 120

Finally, all instances of the Sprite class need to go in a SpriteList class.

self.player_list.append(self.player_sprite)

We manage groups of sprites by the list that they are in. In the example below there’s a wall_list that will hold everything that the player character can’t walk through, and a coin_list for sprites we can pick up to get points. There’s also a player_list which holds only the player.

Notice that the code creates Sprites three ways:

  • Creating a Sprite class, positioning it, adding it to the list

  • Create a series of sprites in a loop

  • Create a series of sprites using coordinates

02_draw_sprites - Draw and Position Sprites
 1"""
 2Platformer Game
 3"""
 4import arcade
 5
 6# Constants
 7SCREEN_WIDTH = 1000
 8SCREEN_HEIGHT = 650
 9SCREEN_TITLE = "Platformer"
10
11# Constants used to scale our sprites from their original size
12CHARACTER_SCALING = 1
13TILE_SCALING = 0.5
14COIN_SCALING = 0.5
15
16
17class MyGame(arcade.Window):
18    """
19    Main application class.
20    """
21
22    def __init__(self):
23
24        # Call the parent class and set up the window
25        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
26
27        # These are 'lists' that keep track of our sprites. Each sprite should
28        # go into a list.
29        self.coin_list = None
30        self.wall_list = None
31        self.player_list = None
32
33        # Separate variable that holds the player sprite
34        self.player_sprite = None
35
36        arcade.set_background_color(arcade.csscolor.CORNFLOWER_BLUE)
37
38    def setup(self):
39        """ Set up the game here. Call this function to restart the game. """
40        # Create the Sprite lists
41        self.player_list = arcade.SpriteList()
42        self.wall_list = arcade.SpriteList(use_spatial_hash=True)
43        self.coin_list = arcade.SpriteList(use_spatial_hash=True)
44
45        # Set up the player, specifically placing it at these coordinates.
46        image_source = ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
47        self.player_sprite = arcade.Sprite(image_source, CHARACTER_SCALING)
48        self.player_sprite.center_x = 64
49        self.player_sprite.center_y = 128
50        self.player_list.append(self.player_sprite)
51
52        # Create the ground
53        # This shows using a loop to place multiple sprites horizontally
54        for x in range(0, 1250, 64):
55            wall = arcade.Sprite(":resources:images/tiles/grassMid.png", TILE_SCALING)
56            wall.center_x = x
57            wall.center_y = 32
58            self.wall_list.append(wall)
59
60        # Put some crates on the ground
61        # This shows using a coordinate list to place sprites
62        coordinate_list = [[512, 96],
63                           [256, 96],
64                           [768, 96]]
65
66        for coordinate in coordinate_list:
67            # Add a crate on the ground
68            wall = arcade.Sprite(":resources:images/tiles/boxCrate_double.png", TILE_SCALING)
69            wall.position = coordinate
70            self.wall_list.append(wall)
71
72    def on_draw(self):
73        """ Render the screen. """
74
75        # Clear the screen to the background color
76        arcade.start_render()
77
78        # Draw our sprites
79        self.wall_list.draw()
80        self.coin_list.draw()
81        self.player_list.draw()
82
83
84def main():
85    """ Main method """
86    window = MyGame()
87    window.setup()
88    arcade.run()
89
90
91if __name__ == "__main__":
92    main()

Note

Once the code example is up and working:

  • Adjust the code and try putting sprites in new positions.

  • Use different images for sprites (see the images folder).

  • Practice placing individually, via a loop, and by coordinates in a list.

Step 3 - Add User Control

Now we need to be able to get the user to move around.

First, at the top of the program add a constant that controls how many pixels per update our character travels:

03_user_control.py - Player Move Speed Constant
# Movement speed of player, in pixels per frame
PLAYER_MOVEMENT_SPEED = 5

Next, at the end of our setup method, we are need to create a physics engine that will move our player and keep her from running through walls. The PhysicsEngineSimple class takes two parameters: The moving sprite, and a list of sprites the moving sprite can’t move through.

For more information about the physics engine we are using here, see PhysicsEngineSimple class

Note

It is possible to have multiple physics engines, one per moving sprite. These are very simple, but easy physics engines. See Pymunk Platformer for a more advanced physics engine.

03_user_control.py - Create Physics Engine
# Movement speed of player, in pixels per frame
PLAYER_MOVEMENT_SPEED = 5

Each sprite has center_x and center_y attributes. Changing these will change the location of the sprite. (There are also attributes for top, bottom, left, right, and angle that will move the sprite.)

Each sprite has change_x and change_y variables. These can be used to hold the velocity that the sprite is moving with. We will adjust these based on what key the user hits. If the user hits the right arrow key we want a positive value for change_x. If the value is 5, it will move 5 pixels per frame.

In this case, when the user presses a key we’ll change the sprites change x and y. The physics engine will look at that, and move the player unless she’ll hit a wall.

03_user_control.py - Handle key-down
 1    def on_key_press(self, key, modifiers):
 2        """Called whenever a key is pressed. """
 3
 4        if key == arcade.key.UP or key == arcade.key.W:
 5            self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
 6        elif key == arcade.key.DOWN or key == arcade.key.S:
 7            self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
 8        elif key == arcade.key.LEFT or key == arcade.key.A:
 9            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
10        elif key == arcade.key.RIGHT or key == arcade.key.D:
11            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED

On releasing the key, we’ll put our speed back to zero.

03_user_control.py - Handle key-up
 1    def on_key_release(self, key, modifiers):
 2        """Called when the user releases a key. """
 3
 4        if key == arcade.key.UP or key == arcade.key.W:
 5            self.player_sprite.change_y = 0
 6        elif key == arcade.key.DOWN or key == arcade.key.S:
 7            self.player_sprite.change_y = 0
 8        elif key == arcade.key.LEFT or key == arcade.key.A:
 9            self.player_sprite.change_x = 0
10        elif key == arcade.key.RIGHT or key == arcade.key.D:
11            self.player_sprite.change_x = 0

Note

This method of tracking the speed to the key the player presses is simple, but isn’t perfect. If the player hits both left and right keys at the same time, then lets off the left one, we expect the player to move right. This method won’t support that. If you want a slightly more complex method that does, see Better Move By Keyboard.

Our on_update method is called about 60 times per second. We’ll ask the physics engine to move our player based on her change_x and change_y.

03_user_control.py - Update the sprites
1    def on_update(self, delta_time):
2        """ Movement and game logic """
3
4        # Move the player with the physics engine
5        self.physics_engine.update()

Step 4 - Add Gravity

The example above works great for top-down, but what if it is a side view with jumping? We need to add gravity. First, let’s define a constant to represent the acceleration for gravity, and one for a jump speed.

04_add_gravity.py - Add Gravity
GRAVITY = 1
PLAYER_JUMP_SPEED = 20

At the end of the setup method, change the physics engine to PhysicsEnginePlatformer and include gravity as a parameter.

04_add_gravity.py - Add Gravity
        # Create the 'physics engine'
        self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
                                                             self.wall_list,
                                                             GRAVITY)

Then, modify the key down and key up event handlers. We’ll remove the up/down statements we had before, and make ‘UP’ jump when pressed.

04_add_gravity.py - Add Gravity
 1    def on_key_press(self, key, modifiers):
 2        """Called whenever a key is pressed. """
 3
 4        if key == arcade.key.UP or key == arcade.key.W:
 5            if self.physics_engine.can_jump():
 6                self.player_sprite.change_y = PLAYER_JUMP_SPEED
 7        elif key == arcade.key.LEFT or key == arcade.key.A:
 8            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
 9        elif key == arcade.key.RIGHT or key == arcade.key.D:
10            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
11
12    def on_key_release(self, key, modifiers):
13        """Called when the user releases a key. """
14
15        if key == arcade.key.LEFT or key == arcade.key.A:
16            self.player_sprite.change_x = 0
17        elif key == arcade.key.RIGHT or key == arcade.key.D:
18            self.player_sprite.change_x = 0

Note

You can change how the user jumps by changing the gravity and jump constants. Lower values for both will make for a more “floaty” character. Higher values make for a faster-paced game.

Step 5 - Add Scrolling

We can have our window be a small viewport into a much larger world by adding scrolling.

The viewport margins control how close you can get to the edge of the screen before the camera starts scrolling.

Add Scrolling
  1"""
  2Platformer Game
  3"""
  4import arcade
  5
  6# Constants
  7SCREEN_WIDTH = 1000
  8SCREEN_HEIGHT = 650
  9SCREEN_TITLE = "Platformer"
 10
 11# Constants used to scale our sprites from their original size
 12CHARACTER_SCALING = 1
 13TILE_SCALING = 0.5
 14COIN_SCALING = 0.5
 15
 16# Movement speed of player, in pixels per frame
 17PLAYER_MOVEMENT_SPEED = 5
 18GRAVITY = 1
 19PLAYER_JUMP_SPEED = 20
 20
 21# How many pixels to keep as a minimum margin between the character
 22# and the edge of the screen.
 23LEFT_VIEWPORT_MARGIN = 250
 24RIGHT_VIEWPORT_MARGIN = 250
 25BOTTOM_VIEWPORT_MARGIN = 50
 26TOP_VIEWPORT_MARGIN = 100
 27
 28
 29class MyGame(arcade.Window):
 30    """
 31    Main application class.
 32    """
 33
 34    def __init__(self):
 35
 36        # Call the parent class and set up the window
 37        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 38
 39        # These are 'lists' that keep track of our sprites. Each sprite should
 40        # go into a list.
 41        self.coin_list = None
 42        self.wall_list = None
 43        self.player_list = None
 44
 45        # Separate variable that holds the player sprite
 46        self.player_sprite = None
 47
 48        # Our physics engine
 49        self.physics_engine = None
 50
 51        # Used to keep track of our scrolling
 52        self.view_bottom = 0
 53        self.view_left = 0
 54
 55        arcade.set_background_color(arcade.csscolor.CORNFLOWER_BLUE)
 56
 57    def setup(self):
 58        """ Set up the game here. Call this function to restart the game. """
 59
 60        # Used to keep track of our scrolling
 61        self.view_bottom = 0
 62        self.view_left = 0
 63
 64        # Create the Sprite lists
 65        self.player_list = arcade.SpriteList()
 66        self.wall_list = arcade.SpriteList()
 67        self.coin_list = arcade.SpriteList()
 68
 69        # Set up the player, specifically placing it at these coordinates.
 70        image_source = ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
 71        self.player_sprite = arcade.Sprite(image_source, CHARACTER_SCALING)
 72        self.player_sprite.center_x = 64
 73        self.player_sprite.center_y = 96
 74        self.player_list.append(self.player_sprite)
 75
 76        # Create the ground
 77        # This shows using a loop to place multiple sprites horizontally
 78        for x in range(0, 1250, 64):
 79            wall = arcade.Sprite(":resources:images/tiles/grassMid.png", TILE_SCALING)
 80            wall.center_x = x
 81            wall.center_y = 32
 82            self.wall_list.append(wall)
 83
 84        # Put some crates on the ground
 85        # This shows using a coordinate list to place sprites
 86        coordinate_list = [[512, 96],
 87                           [256, 96],
 88                           [768, 96]]
 89
 90        for coordinate in coordinate_list:
 91            # Add a crate on the ground
 92            wall = arcade.Sprite(":resources:images/tiles/boxCrate_double.png", TILE_SCALING)
 93            wall.position = coordinate
 94            self.wall_list.append(wall)
 95
 96        # Create the 'physics engine'
 97        self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
 98                                                             self.wall_list,
 99                                                             GRAVITY)
100
101    def on_draw(self):
102        """ Render the screen. """
103
104        # Clear the screen to the background color
105        arcade.start_render()
106
107        # Draw our sprites
108        self.wall_list.draw()
109        self.coin_list.draw()
110        self.player_list.draw()
111
112    def on_key_press(self, key, modifiers):
113        """Called whenever a key is pressed. """
114
115        if key == arcade.key.UP or key == arcade.key.W:
116            if self.physics_engine.can_jump():
117                self.player_sprite.change_y = PLAYER_JUMP_SPEED
118        elif key == arcade.key.LEFT or key == arcade.key.A:
119            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
120        elif key == arcade.key.RIGHT or key == arcade.key.D:
121            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
122
123    def on_key_release(self, key, modifiers):
124        """Called when the user releases a key. """
125
126        if key == arcade.key.LEFT or key == arcade.key.A:
127            self.player_sprite.change_x = 0
128        elif key == arcade.key.RIGHT or key == arcade.key.D:
129            self.player_sprite.change_x = 0
130
131    def on_update(self, delta_time):
132        """ Movement and game logic """
133
134        # Move the player with the physics engine
135        self.physics_engine.update()
136
137        # --- Manage Scrolling ---
138
139        # Track if we need to change the viewport
140
141        changed = False
142
143        # Scroll left
144        left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
145        if self.player_sprite.left < left_boundary:
146            self.view_left -= left_boundary - self.player_sprite.left
147            changed = True
148
149        # Scroll right
150        right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
151        if self.player_sprite.right > right_boundary:
152            self.view_left += self.player_sprite.right - right_boundary
153            changed = True
154
155        # Scroll up
156        top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
157        if self.player_sprite.top > top_boundary:
158            self.view_bottom += self.player_sprite.top - top_boundary
159            changed = True
160
161        # Scroll down
162        bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
163        if self.player_sprite.bottom < bottom_boundary:
164            self.view_bottom -= bottom_boundary - self.player_sprite.bottom
165            changed = True
166
167        if changed:
168            # Only scroll to integers. Otherwise we end up with pixels that
169            # don't line up on the screen
170            self.view_bottom = int(self.view_bottom)
171            self.view_left = int(self.view_left)
172
173            # Do the scrolling
174            arcade.set_viewport(self.view_left,
175                                SCREEN_WIDTH + self.view_left,
176                                self.view_bottom,
177                                SCREEN_HEIGHT + self.view_bottom)
178
179
180def main():
181    """ Main method """
182    window = MyGame()
183    window.setup()
184    arcade.run()
185
186
187if __name__ == "__main__":
188    main()

Note

Work at changing the viewport margins to something that you like.

Step 6 - Add Coins And Sound

_images/listing_06.png

The code below adds coins that we can collect. It also adds a sound to be played when the user hits a coin, or presses the jump button.

We check to see if the user hits a coin by the arcade.check_for_collision_with_list function. Just pass the player sprite, along with a SpriteList that holds the coins. The function returns a list of coins in contact with the player sprite. If no coins are in contact, the list is empty.

The method Sprite.remove_from_sprite_lists will remove that sprite from all lists, and effectively the game.

Notice that any transparent “white-space” around the image counts as the hitbox. You can trim the space in a graphics editor, or in the second section, we’ll show you how to specify the hitbox.

Add Coins and Sound
  1"""
  2Platformer Game
  3"""
  4import arcade
  5
  6# Constants
  7SCREEN_WIDTH = 1000
  8SCREEN_HEIGHT = 650
  9SCREEN_TITLE = "Platformer"
 10
 11# Constants used to scale our sprites from their original size
 12CHARACTER_SCALING = 1
 13TILE_SCALING = 0.5
 14COIN_SCALING = 0.5
 15
 16# Movement speed of player, in pixels per frame
 17PLAYER_MOVEMENT_SPEED = 5
 18GRAVITY = 1
 19PLAYER_JUMP_SPEED = 20
 20
 21# How many pixels to keep as a minimum margin between the character
 22# and the edge of the screen.
 23LEFT_VIEWPORT_MARGIN = 250
 24RIGHT_VIEWPORT_MARGIN = 250
 25BOTTOM_VIEWPORT_MARGIN = 50
 26TOP_VIEWPORT_MARGIN = 100
 27
 28
 29class MyGame(arcade.Window):
 30    """
 31    Main application class.
 32    """
 33
 34    def __init__(self):
 35
 36        # Call the parent class and set up the window
 37        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 38
 39        # These are 'lists' that keep track of our sprites. Each sprite should
 40        # go into a list.
 41        self.coin_list = None
 42        self.wall_list = None
 43        self.player_list = None
 44
 45        # Separate variable that holds the player sprite
 46        self.player_sprite = None
 47
 48        # Our physics engine
 49        self.physics_engine = None
 50
 51        # Used to keep track of our scrolling
 52        self.view_bottom = 0
 53        self.view_left = 0
 54
 55        # Load sounds
 56        self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
 57        self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
 58
 59        arcade.set_background_color(arcade.csscolor.CORNFLOWER_BLUE)
 60
 61    def setup(self):
 62        """ Set up the game here. Call this function to restart the game. """
 63
 64        # Used to keep track of our scrolling
 65        self.view_bottom = 0
 66        self.view_left = 0
 67
 68        # Create the Sprite lists
 69        self.player_list = arcade.SpriteList()
 70        self.wall_list = arcade.SpriteList()
 71        self.coin_list = arcade.SpriteList()
 72
 73        # Set up the player, specifically placing it at these coordinates.
 74        image_source = ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
 75        self.player_sprite = arcade.Sprite(image_source, CHARACTER_SCALING)
 76        self.player_sprite.center_x = 64
 77        self.player_sprite.center_y = 128
 78        self.player_list.append(self.player_sprite)
 79
 80        # Create the ground
 81        # This shows using a loop to place multiple sprites horizontally
 82        for x in range(0, 1250, 64):
 83            wall = arcade.Sprite(":resources:images/tiles/grassMid.png", TILE_SCALING)
 84            wall.center_x = x
 85            wall.center_y = 32
 86            self.wall_list.append(wall)
 87
 88        # Put some crates on the ground
 89        # This shows using a coordinate list to place sprites
 90        coordinate_list = [[512, 96],
 91                           [256, 96],
 92                           [768, 96]]
 93
 94        for coordinate in coordinate_list:
 95            # Add a crate on the ground
 96            wall = arcade.Sprite(":resources:images/tiles/boxCrate_double.png", TILE_SCALING)
 97            wall.position = coordinate
 98            self.wall_list.append(wall)
 99
100        # Use a loop to place some coins for our character to pick up
101        for x in range(128, 1250, 256):
102            coin = arcade.Sprite(":resources:images/items/coinGold.png", COIN_SCALING)
103            coin.center_x = x
104            coin.center_y = 96
105            self.coin_list.append(coin)
106
107        # Create the 'physics engine'
108        self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
109                                                             self.wall_list,
110                                                             GRAVITY)
111
112    def on_draw(self):
113        """ Render the screen. """
114
115        # Clear the screen to the background color
116        arcade.start_render()
117
118        # Draw our sprites
119        self.wall_list.draw()
120        self.coin_list.draw()
121        self.player_list.draw()
122
123    def on_key_press(self, key, modifiers):
124        """Called whenever a key is pressed. """
125
126        if key == arcade.key.UP or key == arcade.key.W:
127            if self.physics_engine.can_jump():
128                self.player_sprite.change_y = PLAYER_JUMP_SPEED
129                arcade.play_sound(self.jump_sound)
130        elif key == arcade.key.LEFT or key == arcade.key.A:
131            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
132        elif key == arcade.key.RIGHT or key == arcade.key.D:
133            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
134
135    def on_key_release(self, key, modifiers):
136        """Called when the user releases a key. """
137
138        if key == arcade.key.LEFT or key == arcade.key.A:
139            self.player_sprite.change_x = 0
140        elif key == arcade.key.RIGHT or key == arcade.key.D:
141            self.player_sprite.change_x = 0
142
143    def update(self, delta_time):
144        """ Movement and game logic """
145
146        # Move the player with the physics engine
147        self.physics_engine.update()
148
149        # See if we hit any coins
150        coin_hit_list = arcade.check_for_collision_with_list(self.player_sprite,
151                                                             self.coin_list)
152
153        # Loop through each coin we hit (if any) and remove it
154        for coin in coin_hit_list:
155            # Remove the coin
156            coin.remove_from_sprite_lists()
157            # Play a sound
158            arcade.play_sound(self.collect_coin_sound)
159
160        # --- Manage Scrolling ---
161
162        # Track if we need to change the viewport
163
164        changed = False
165
166        # Scroll left
167        left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
168        if self.player_sprite.left < left_boundary:
169            self.view_left -= left_boundary - self.player_sprite.left
170            changed = True
171
172        # Scroll right
173        right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
174        if self.player_sprite.right > right_boundary:
175            self.view_left += self.player_sprite.right - right_boundary
176            changed = True
177
178        # Scroll up
179        top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
180        if self.player_sprite.top > top_boundary:
181            self.view_bottom += self.player_sprite.top - top_boundary
182            changed = True
183
184        # Scroll down
185        bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
186        if self.player_sprite.bottom < bottom_boundary:
187            self.view_bottom -= bottom_boundary - self.player_sprite.bottom
188            changed = True
189
190        if changed:
191            # Only scroll to integers. Otherwise we end up with pixels that
192            # don't line up on the screen
193            self.view_bottom = int(self.view_bottom)
194            self.view_left = int(self.view_left)
195
196            # Do the scrolling
197            arcade.set_viewport(self.view_left,
198                                SCREEN_WIDTH + self.view_left,
199                                self.view_bottom,
200                                SCREEN_HEIGHT + self.view_bottom)
201
202
203def main():
204    """ Main method """
205    window = MyGame()
206    window.setup()
207    arcade.run()
208
209
210if __name__ == "__main__":
211    main()

Note

Spend time placing the coins where you would like them. If you have extra time, try adding more than just coins. Also add gems or keys from the graphics provided.

You could also subclass the coin sprite and add an attribute for a score value. Then you could have coins worth one point, and gems worth 5, 10, and 15 points.

Step 7 - Display The Score

Now that we can collect coins and get points, we need a way to display the score on the screen.

This is a bit more complex than just drawing the score at the same x, y location every time because we have to “scroll” the score right with the player if we have a scrolling screen. To do this, we just add in the view_bottom and view_left coordinates.

Display The Score
  1"""
  2Platformer Game
  3"""
  4import arcade
  5
  6# Constants
  7SCREEN_WIDTH = 1000
  8SCREEN_HEIGHT = 650
  9SCREEN_TITLE = "Platformer"
 10
 11# Constants used to scale our sprites from their original size
 12CHARACTER_SCALING = 1
 13TILE_SCALING = 0.5
 14COIN_SCALING = 0.5
 15
 16# Movement speed of player, in pixels per frame
 17PLAYER_MOVEMENT_SPEED = 5
 18GRAVITY = 1
 19PLAYER_JUMP_SPEED = 20
 20
 21# How many pixels to keep as a minimum margin between the character
 22# and the edge of the screen.
 23LEFT_VIEWPORT_MARGIN = 250
 24RIGHT_VIEWPORT_MARGIN = 250
 25BOTTOM_VIEWPORT_MARGIN = 50
 26TOP_VIEWPORT_MARGIN = 100
 27
 28
 29class MyGame(arcade.Window):
 30    """
 31    Main application class.
 32    """
 33
 34    def __init__(self):
 35
 36        # Call the parent class and set up the window
 37        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 38
 39        # These are 'lists' that keep track of our sprites. Each sprite should
 40        # go into a list.
 41        self.coin_list = None
 42        self.wall_list = None
 43        self.player_list = None
 44
 45        # Separate variable that holds the player sprite
 46        self.player_sprite = None
 47
 48        # Our physics engine
 49        self.physics_engine = None
 50
 51        # Used to keep track of our scrolling
 52        self.view_bottom = 0
 53        self.view_left = 0
 54
 55        # Keep track of the score
 56        self.score = 0
 57
 58        # Load sounds
 59        self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
 60        self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
 61
 62        arcade.set_background_color(arcade.csscolor.CORNFLOWER_BLUE)
 63
 64    def setup(self):
 65        """ Set up the game here. Call this function to restart the game. """
 66
 67        # Used to keep track of our scrolling
 68        self.view_bottom = 0
 69        self.view_left = 0
 70
 71        # Keep track of the score
 72        self.score = 0
 73
 74        # Create the Sprite lists
 75        self.player_list = arcade.SpriteList()
 76        self.wall_list = arcade.SpriteList()
 77        self.coin_list = arcade.SpriteList()
 78
 79        # Set up the player, specifically placing it at these coordinates.
 80        image_source = ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
 81        self.player_sprite = arcade.Sprite(image_source, CHARACTER_SCALING)
 82        self.player_sprite.center_x = 64
 83        self.player_sprite.center_y = 96
 84        self.player_list.append(self.player_sprite)
 85
 86        # Create the ground
 87        # This shows using a loop to place multiple sprites horizontally
 88        for x in range(0, 1250, 64):
 89            wall = arcade.Sprite(":resources:images/tiles/grassMid.png", TILE_SCALING)
 90            wall.center_x = x
 91            wall.center_y = 32
 92            self.wall_list.append(wall)
 93
 94        # Put some crates on the ground
 95        # This shows using a coordinate list to place sprites
 96        coordinate_list = [[512, 96],
 97                           [256, 96],
 98                           [768, 96]]
 99
100        for coordinate in coordinate_list:
101            # Add a crate on the ground
102            wall = arcade.Sprite(":resources:images/tiles/boxCrate_double.png", TILE_SCALING)
103            wall.position = coordinate
104            self.wall_list.append(wall)
105
106        # Use a loop to place some coins for our character to pick up
107        for x in range(128, 1250, 256):
108            coin = arcade.Sprite(":resources:images/items/coinGold.png", COIN_SCALING)
109            coin.center_x = x
110            coin.center_y = 96
111            self.coin_list.append(coin)
112
113        # Create the 'physics engine'
114        self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
115                                                             self.wall_list,
116                                                             GRAVITY)
117
118    def on_draw(self):
119        """ Render the screen. """
120
121        # Clear the screen to the background color
122        arcade.start_render()
123
124        # Draw our sprites
125        self.wall_list.draw()
126        self.coin_list.draw()
127        self.player_list.draw()
128
129        # Draw our score on the screen, scrolling it with the viewport
130        score_text = f"Score: {self.score}"
131        arcade.draw_text(score_text, 10 + self.view_left, 10 + self.view_bottom,
132                         arcade.csscolor.WHITE, 18)
133
134    def on_key_press(self, key, modifiers):
135        """Called whenever a key is pressed. """
136
137        if key == arcade.key.UP or key == arcade.key.W:
138            if self.physics_engine.can_jump():
139                self.player_sprite.change_y = PLAYER_JUMP_SPEED
140                arcade.play_sound(self.jump_sound)
141        elif key == arcade.key.LEFT or key == arcade.key.A:
142            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
143        elif key == arcade.key.RIGHT or key == arcade.key.D:
144            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
145
146    def on_key_release(self, key, modifiers):
147        """Called when the user releases a key. """
148
149        if key == arcade.key.LEFT or key == arcade.key.A:
150            self.player_sprite.change_x = 0
151        elif key == arcade.key.RIGHT or key == arcade.key.D:
152            self.player_sprite.change_x = 0
153
154    def on_update(self, delta_time):
155        """ Movement and game logic """
156
157        # Move the player with the physics engine
158        self.physics_engine.update()
159
160        # See if we hit any coins
161        coin_hit_list = arcade.check_for_collision_with_list(self.player_sprite,
162                                                             self.coin_list)
163
164        # Loop through each coin we hit (if any) and remove it
165        for coin in coin_hit_list:
166            # Remove the coin
167            coin.remove_from_sprite_lists()
168            # Play a sound
169            arcade.play_sound(self.collect_coin_sound)
170            # Add one to the score
171            self.score += 1
172
173        # --- Manage Scrolling ---
174
175        # Track if we need to change the viewport
176
177        changed = False
178
179        # Scroll left
180        left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
181        if self.player_sprite.left < left_boundary:
182            self.view_left -= left_boundary - self.player_sprite.left
183            changed = True
184
185        # Scroll right
186        right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
187        if self.player_sprite.right > right_boundary:
188            self.view_left += self.player_sprite.right - right_boundary
189            changed = True
190
191        # Scroll up
192        top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
193        if self.player_sprite.top > top_boundary:
194            self.view_bottom += self.player_sprite.top - top_boundary
195            changed = True
196
197        # Scroll down
198        bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
199        if self.player_sprite.bottom < bottom_boundary:
200            self.view_bottom -= bottom_boundary - self.player_sprite.bottom
201            changed = True
202
203        if changed:
204            # Only scroll to integers. Otherwise we end up with pixels that
205            # don't line up on the screen
206            self.view_bottom = int(self.view_bottom)
207            self.view_left = int(self.view_left)
208
209            # Do the scrolling
210            arcade.set_viewport(self.view_left,
211                                SCREEN_WIDTH + self.view_left,
212                                self.view_bottom,
213                                SCREEN_HEIGHT + self.view_bottom)
214
215
216def main():
217    """ Main method """
218    window = MyGame()
219    window.setup()
220    arcade.run()
221
222
223if __name__ == "__main__":
224    main()

Note

You might also want to add:

Explore On Your Own

Step 8 - Use a Map Editor

_images/use_tileset.png
Create a Map File

For this part, we’ll restart with a new program. Instead of placing our tiles by code, we’ll use a map editor.

Download and install the Tiled Map Editor. (Think about donating, as it is a wonderful project.)

Open a new file with options similar to these:

  • Orthogonal - This is a normal square-grid layout. It is the only version that Arcade supports very well at this time.

  • Tile layer format - This selects how the data is stored inside the file. Any option works, but Base64 zlib compressed is the smallest.

  • Tile render order - Any of these should work. It simply specifies what order the tiles are added. Right-down has tiles added left->right and top->down.

  • Map size - You can change this later, but this is your total grid size.

  • Tile size - the size, in pixels, of your tiles. Your tiles all need to be the same size. Also, rendering works better if the tile size is a power of 2, such as 16, 32, 64, 128, and 256.

_images/new_file.png

Save it as map.tmx.

Rename the layer “Platforms”. We’ll use layer names to load our data later. Eventually you might have layers for:

  • Platforms that you run into (or you can think of them as walls)

  • Coins or objects to pick up

  • Background objects that you don’t interact with, but appear behind the player

  • Foreground objects that you don’t interact with, but appear in front of the player

  • Insta-death blocks (like lava)

  • Ladders

Note

Once you get multiple layers it is VERY easy to add items to the wrong layer.

_images/platforms.png
Create a Tileset

Before we can add anything to the layer we need to create a set of tiles. This isn’t as obvious or intuitive as it should be. To create a new tileset click “New Tileset” in the window on the lower right:

_images/new_tileset.png

Right now, Arcade only supports a “collection of images” for a tileset. I find it convenient to embed the tileset in the map.

_images/new_tileset_02.png

Once you create a new tile, the button to add tiles to the tileset is hard to find. Click the wrench:

_images/new_tileset_03.png

Then click the ‘plus’ and add in your tiles

_images/new_tileset_04.png
Draw a Level

At this point you should be able to “paint” a level. At the very least, put in a floor and then see if you can get this program working. (Don’t put in a lot of time designing a level until you are sure you can get it to load.)

The program below assumes there are layers created by the tiled map editor for for “Platforms” and “Coins”.

Test the Level
Load a .tmx file from Tiled Map Editor
  1"""
  2Platformer Game
  3"""
  4import arcade
  5
  6# Constants
  7SCREEN_WIDTH = 1000
  8SCREEN_HEIGHT = 650
  9SCREEN_TITLE = "Platformer"
 10
 11# Constants used to scale our sprites from their original size
 12CHARACTER_SCALING = 1
 13TILE_SCALING = 0.5
 14COIN_SCALING = 0.5
 15SPRITE_PIXEL_SIZE = 128
 16GRID_PIXEL_SIZE = (SPRITE_PIXEL_SIZE * TILE_SCALING)
 17
 18# Movement speed of player, in pixels per frame
 19PLAYER_MOVEMENT_SPEED = 10
 20GRAVITY = 1
 21PLAYER_JUMP_SPEED = 20
 22
 23# How many pixels to keep as a minimum margin between the character
 24# and the edge of the screen.
 25LEFT_VIEWPORT_MARGIN = 250
 26RIGHT_VIEWPORT_MARGIN = 250
 27BOTTOM_VIEWPORT_MARGIN = 100
 28TOP_VIEWPORT_MARGIN = 100
 29
 30
 31class MyGame(arcade.Window):
 32    """
 33    Main application class.
 34    """
 35
 36    def __init__(self):
 37
 38        # Call the parent class and set up the window
 39        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 40
 41        # These are 'lists' that keep track of our sprites. Each sprite should
 42        # go into a list.
 43        self.coin_list = None
 44        self.wall_list = None
 45        self.player_list = None
 46
 47        # Separate variable that holds the player sprite
 48        self.player_sprite = None
 49
 50        # Our physics engine
 51        self.physics_engine = None
 52
 53        # Used to keep track of our scrolling
 54        self.view_bottom = 0
 55        self.view_left = 0
 56
 57        # Keep track of the score
 58        self.score = 0
 59
 60        # Load sounds
 61        self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
 62        self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
 63
 64        arcade.set_background_color(arcade.csscolor.CORNFLOWER_BLUE)
 65
 66    def setup(self):
 67        """ Set up the game here. Call this function to restart the game. """
 68
 69        # Used to keep track of our scrolling
 70        self.view_bottom = 0
 71        self.view_left = 0
 72
 73        # Keep track of the score
 74        self.score = 0
 75
 76        # Create the Sprite lists
 77        self.player_list = arcade.SpriteList()
 78        self.wall_list = arcade.SpriteList()
 79        self.coin_list = arcade.SpriteList()
 80
 81        # Set up the player, specifically placing it at these coordinates.
 82        image_source = ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
 83        self.player_sprite = arcade.Sprite(image_source, CHARACTER_SCALING)
 84        self.player_sprite.center_x = 128
 85        self.player_sprite.center_y = 128
 86        self.player_list.append(self.player_sprite)
 87
 88        # --- Load in a map from the tiled editor ---
 89
 90        # Name of map file to load
 91        map_name = ":resources:tmx_maps/map.tmx"
 92        # Name of the layer in the file that has our platforms/walls
 93        platforms_layer_name = 'Platforms'
 94        # Name of the layer that has items for pick-up
 95        coins_layer_name = 'Coins'
 96
 97        # Read in the tiled map
 98        my_map = arcade.tilemap.read_tmx(map_name)
 99
100        # -- Platforms
101        self.wall_list = arcade.tilemap.process_layer(map_object=my_map,
102                                                      layer_name=platforms_layer_name,
103                                                      scaling=TILE_SCALING,
104                                                      use_spatial_hash=True)
105
106        # -- Coins
107        self.coin_list = arcade.tilemap.process_layer(my_map, coins_layer_name, TILE_SCALING)
108
109        # --- Other stuff
110        # Set the background color
111        if my_map.background_color:
112            arcade.set_background_color(my_map.background_color)
113
114        # Create the 'physics engine'
115        self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
116                                                             self.wall_list,
117                                                             GRAVITY)
118
119    def on_draw(self):
120        """ Render the screen. """
121
122        # Clear the screen to the background color
123        arcade.start_render()
124
125        # Draw our sprites
126        self.wall_list.draw()
127        self.coin_list.draw()
128        self.player_list.draw()
129
130        # Draw our score on the screen, scrolling it with the viewport
131        score_text = f"Score: {self.score}"
132        arcade.draw_text(score_text, 10 + self.view_left, 10 + self.view_bottom,
133                         arcade.csscolor.WHITE, 18)
134
135    def on_key_press(self, key, modifiers):
136        """Called whenever a key is pressed. """
137
138        if key == arcade.key.UP or key == arcade.key.W:
139            if self.physics_engine.can_jump():
140                self.player_sprite.change_y = PLAYER_JUMP_SPEED
141                arcade.play_sound(self.jump_sound)
142        elif key == arcade.key.LEFT or key == arcade.key.A:
143            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
144        elif key == arcade.key.RIGHT or key == arcade.key.D:
145            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
146
147    def on_key_release(self, key, modifiers):
148        """Called when the user releases a key. """
149
150        if key == arcade.key.LEFT or key == arcade.key.A:
151            self.player_sprite.change_x = 0
152        elif key == arcade.key.RIGHT or key == arcade.key.D:
153            self.player_sprite.change_x = 0
154
155    def on_update(self, delta_time):
156        """ Movement and game logic """
157
158        # Move the player with the physics engine
159        self.physics_engine.update()
160
161        # See if we hit any coins
162        coin_hit_list = arcade.check_for_collision_with_list(self.player_sprite,
163                                                             self.coin_list)
164
165        # Loop through each coin we hit (if any) and remove it
166        for coin in coin_hit_list:
167            # Remove the coin
168            coin.remove_from_sprite_lists()
169            # Play a sound
170            arcade.play_sound(self.collect_coin_sound)
171            # Add one to the score
172            self.score += 1
173
174        # --- Manage Scrolling ---
175
176        # Track if we need to change the viewport
177
178        changed = False
179
180        # Scroll left
181        left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
182        if self.player_sprite.left < left_boundary:
183            self.view_left -= left_boundary - self.player_sprite.left
184            changed = True
185
186        # Scroll right
187        right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
188        if self.player_sprite.right > right_boundary:
189            self.view_left += self.player_sprite.right - right_boundary
190            changed = True
191
192        # Scroll up
193        top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
194        if self.player_sprite.top > top_boundary:
195            self.view_bottom += self.player_sprite.top - top_boundary
196            changed = True
197
198        # Scroll down
199        bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
200        if self.player_sprite.bottom < bottom_boundary:
201            self.view_bottom -= bottom_boundary - self.player_sprite.bottom
202            changed = True
203
204        if changed:
205            # Only scroll to integers. Otherwise we end up with pixels that
206            # don't line up on the screen
207            self.view_bottom = int(self.view_bottom)
208            self.view_left = int(self.view_left)
209
210            # Do the scrolling
211            arcade.set_viewport(self.view_left,
212                                SCREEN_WIDTH + self.view_left,
213                                self.view_bottom,
214                                SCREEN_HEIGHT + self.view_bottom)
215
216
217def main():
218    """ Main method """
219    window = MyGame()
220    window.setup()
221    arcade.run()
222
223
224if __name__ == "__main__":
225    main()

Note

You can set the background color of the map by selecting “Map…Map Properties”. Then click on the three dots to pull up a color picker.

You can edit the hitbox of a tile to make ramps or platforms that only cover a portion of the rectangle in the grid.

To edit the hitbox, use the polygon tool (only) and draw a polygon around the item. You can hold down “CTRL” when positioning a point to get the exact corner of an item.

_images/collision_editor.png

Step 9 - Multiple Levels and Other Layers

Here’s an expanded example:

  • This adds foreground, background, and “Don’t Touch” layers.

    • The background tiles appear behind the player

    • The foreground appears in front of the player

    • The Don’t Touch layer will reset the player to the start (228-237)

  • The player resets to the start if they fall off the map (217-226)

  • If the player gets to the right side of the map, the program attempts to load another layer

    • Add level attribute (69-70)

    • Updated setup to load a file based on the level (76-144, specifically lines 77 and 115)

    • Added end-of-map check(245-256)

More Advanced Example
  1"""
  2Platformer Game
  3"""
  4import arcade
  5
  6# Constants
  7SCREEN_WIDTH = 1000
  8SCREEN_HEIGHT = 650
  9SCREEN_TITLE = "Platformer"
 10
 11# Constants used to scale our sprites from their original size
 12CHARACTER_SCALING = 1
 13TILE_SCALING = 0.5
 14COIN_SCALING = 0.5
 15SPRITE_PIXEL_SIZE = 128
 16GRID_PIXEL_SIZE = (SPRITE_PIXEL_SIZE * TILE_SCALING)
 17
 18# Movement speed of player, in pixels per frame
 19PLAYER_MOVEMENT_SPEED = 10
 20GRAVITY = 1
 21PLAYER_JUMP_SPEED = 20
 22
 23# How many pixels to keep as a minimum margin between the character
 24# and the edge of the screen.
 25LEFT_VIEWPORT_MARGIN = 200
 26RIGHT_VIEWPORT_MARGIN = 200
 27BOTTOM_VIEWPORT_MARGIN = 150
 28TOP_VIEWPORT_MARGIN = 100
 29
 30PLAYER_START_X = 64
 31PLAYER_START_Y = 225
 32
 33
 34class MyGame(arcade.Window):
 35    """
 36    Main application class.
 37    """
 38
 39    def __init__(self):
 40
 41        # Call the parent class and set up the window
 42        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 43
 44        # These are 'lists' that keep track of our sprites. Each sprite should
 45        # go into a list.
 46        self.coin_list = None
 47        self.wall_list = None
 48        self.foreground_list = None
 49        self.background_list = None
 50        self.dont_touch_list = None
 51        self.player_list = None
 52
 53        # Separate variable that holds the player sprite
 54        self.player_sprite = None
 55
 56        # Our physics engine
 57        self.physics_engine = None
 58
 59        # Used to keep track of our scrolling
 60        self.view_bottom = 0
 61        self.view_left = 0
 62
 63        # Keep track of the score
 64        self.score = 0
 65
 66        # Where is the right edge of the map?
 67        self.end_of_map = 0
 68
 69        # Level
 70        self.level = 1
 71
 72        # Load sounds
 73        self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
 74        self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
 75        self.game_over = arcade.load_sound(":resources:sounds/gameover1.wav")
 76
 77    def setup(self, level):
 78        """ Set up the game here. Call this function to restart the game. """
 79
 80        # Used to keep track of our scrolling
 81        self.view_bottom = 0
 82        self.view_left = 0
 83
 84        # Keep track of the score
 85        self.score = 0
 86
 87        # Create the Sprite lists
 88        self.player_list = arcade.SpriteList()
 89        self.foreground_list = arcade.SpriteList()
 90        self.background_list = arcade.SpriteList()
 91        self.wall_list = arcade.SpriteList()
 92        self.coin_list = arcade.SpriteList()
 93
 94        # Set up the player, specifically placing it at these coordinates.
 95        image_source = ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
 96        self.player_sprite = arcade.Sprite(image_source, CHARACTER_SCALING)
 97        self.player_sprite.center_x = PLAYER_START_X
 98        self.player_sprite.center_y = PLAYER_START_Y
 99        self.player_list.append(self.player_sprite)
100
101        # --- Load in a map from the tiled editor ---
102
103        # Name of the layer in the file that has our platforms/walls
104        platforms_layer_name = 'Platforms'
105        # Name of the layer that has items for pick-up
106        coins_layer_name = 'Coins'
107        # Name of the layer that has items for foreground
108        foreground_layer_name = 'Foreground'
109        # Name of the layer that has items for background
110        background_layer_name = 'Background'
111        # Name of the layer that has items we shouldn't touch
112        dont_touch_layer_name = "Don't Touch"
113
114        # Map name
115        map_name = f":resources:tmx_maps/map2_level_{level}.tmx"
116
117        # Read in the tiled map
118        my_map = arcade.tilemap.read_tmx(map_name)
119
120        # Calculate the right edge of the my_map in pixels
121        self.end_of_map = my_map.map_size.width * GRID_PIXEL_SIZE
122
123        # -- Background
124        self.background_list = arcade.tilemap.process_layer(my_map,
125                                                            background_layer_name,
126                                                            TILE_SCALING)
127
128        # -- Foreground
129        self.foreground_list = arcade.tilemap.process_layer(my_map,
130                                                            foreground_layer_name,
131                                                            TILE_SCALING)
132
133        # -- Platforms
134        self.wall_list = arcade.tilemap.process_layer(map_object=my_map,
135                                                      layer_name=platforms_layer_name,
136                                                      scaling=TILE_SCALING,
137                                                      use_spatial_hash=True)
138
139        # -- Coins
140        self.coin_list = arcade.tilemap.process_layer(my_map,
141                                                      coins_layer_name,
142                                                      TILE_SCALING,
143                                                      use_spatial_hash=True)
144
145        # -- Don't Touch Layer
146        self.dont_touch_list = arcade.tilemap.process_layer(my_map,
147                                                            dont_touch_layer_name,
148                                                            TILE_SCALING,
149                                                            use_spatial_hash=True)
150
151        # --- Other stuff
152        # Set the background color
153        if my_map.background_color:
154            arcade.set_background_color(my_map.background_color)
155
156        # Create the 'physics engine'
157        self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
158                                                             self.wall_list,
159                                                             GRAVITY)
160
161    def on_draw(self):
162        """ Render the screen. """
163
164        # Clear the screen to the background color
165        arcade.start_render()
166
167        # Draw our sprites
168        self.wall_list.draw()
169        self.background_list.draw()
170        self.wall_list.draw()
171        self.coin_list.draw()
172        self.dont_touch_list.draw()
173        self.player_list.draw()
174        self.foreground_list.draw()
175
176        # Draw our score on the screen, scrolling it with the viewport
177        score_text = f"Score: {self.score}"
178        arcade.draw_text(score_text, 10 + self.view_left, 10 + self.view_bottom,
179                         arcade.csscolor.BLACK, 18)
180
181    def on_key_press(self, key, modifiers):
182        """Called whenever a key is pressed. """
183
184        if key == arcade.key.UP or key == arcade.key.W:
185            if self.physics_engine.can_jump():
186                self.player_sprite.change_y = PLAYER_JUMP_SPEED
187                arcade.play_sound(self.jump_sound)
188        elif key == arcade.key.LEFT or key == arcade.key.A:
189            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
190        elif key == arcade.key.RIGHT or key == arcade.key.D:
191            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
192
193    def on_key_release(self, key, modifiers):
194        """Called when the user releases a key. """
195
196        if key == arcade.key.LEFT or key == arcade.key.A:
197            self.player_sprite.change_x = 0
198        elif key == arcade.key.RIGHT or key == arcade.key.D:
199            self.player_sprite.change_x = 0
200
201    def update(self, delta_time):
202        """ Movement and game logic """
203
204        # Move the player with the physics engine
205        self.physics_engine.update()
206
207        # See if we hit any coins
208        coin_hit_list = arcade.check_for_collision_with_list(self.player_sprite,
209                                                             self.coin_list)
210
211        # Loop through each coin we hit (if any) and remove it
212        for coin in coin_hit_list:
213            # Remove the coin
214            coin.remove_from_sprite_lists()
215            # Play a sound
216            arcade.play_sound(self.collect_coin_sound)
217            # Add one to the score
218            self.score += 1
219
220        # Track if we need to change the viewport
221        changed_viewport = False
222
223        # Did the player fall off the map?
224        if self.player_sprite.center_y < -100:
225            self.player_sprite.center_x = PLAYER_START_X
226            self.player_sprite.center_y = PLAYER_START_Y
227
228            # Set the camera to the start
229            self.view_left = 0
230            self.view_bottom = 0
231            changed_viewport = True
232            arcade.play_sound(self.game_over)
233
234        # Did the player touch something they should not?
235        if arcade.check_for_collision_with_list(self.player_sprite,
236                                                self.dont_touch_list):
237            self.player_sprite.change_x = 0
238            self.player_sprite.change_y = 0
239            self.player_sprite.center_x = PLAYER_START_X
240            self.player_sprite.center_y = PLAYER_START_Y
241
242            # Set the camera to the start
243            self.view_left = 0
244            self.view_bottom = 0
245            changed_viewport = True
246            arcade.play_sound(self.game_over)
247
248        # See if the user got to the end of the level
249        if self.player_sprite.center_x >= self.end_of_map:
250            # Advance to the next level
251            self.level += 1
252
253            # Load the next level
254            self.setup(self.level)
255
256            # Set the camera to the start
257            self.view_left = 0
258            self.view_bottom = 0
259            changed_viewport = True
260
261        # --- Manage Scrolling ---
262
263        # Scroll left
264        left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
265        if self.player_sprite.left < left_boundary:
266            self.view_left -= left_boundary - self.player_sprite.left
267            changed_viewport = True
268
269        # Scroll right
270        right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
271        if self.player_sprite.right > right_boundary:
272            self.view_left += self.player_sprite.right - right_boundary
273            changed_viewport = True
274
275        # Scroll up
276        top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
277        if self.player_sprite.top > top_boundary:
278            self.view_bottom += self.player_sprite.top - top_boundary
279            changed_viewport = True
280
281        # Scroll down
282        bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
283        if self.player_sprite.bottom < bottom_boundary:
284            self.view_bottom -= bottom_boundary - self.player_sprite.bottom
285            changed_viewport = True
286
287        if changed_viewport:
288            # Only scroll to integers. Otherwise we end up with pixels that
289            # don't line up on the screen
290            self.view_bottom = int(self.view_bottom)
291            self.view_left = int(self.view_left)
292
293            # Do the scrolling
294            arcade.set_viewport(self.view_left,
295                                SCREEN_WIDTH + self.view_left,
296                                self.view_bottom,
297                                SCREEN_HEIGHT + self.view_bottom)
298
299
300def main():
301    """ Main method """
302    window = MyGame()
303    window.setup(window.level)
304    arcade.run()
305
306
307if __name__ == "__main__":
308    main()

Step 10 - Add Ladders, Properties, and a Moving Platform

_images/10_ladders_and_more2.png

This example shows using:

  • Ladders

  • Properties to define point value of coins and flags

  • Properties and an object layer to define a moving platform.

To create a moving platform using TMX editor, there are a few steps:

  1. Define an object layer instead of a tile layer.

  2. Select Insert Tile

  3. Select the tile you wish to insert.

  4. Place the tile.

  5. Add custom properties. You can add:

  • change_x

  • change_y

  • boundary_bottom

  • boundary_top

  • boundary_left

  • boundary_right

_images/moving_platform_setup.png
Ladders, Animated Tiles, and Moving Platforms
  1"""
  2Platformer Game
  3"""
  4import arcade
  5import os
  6
  7# Constants
  8SCREEN_WIDTH = 1000
  9SCREEN_HEIGHT = 650
 10SCREEN_TITLE = "Platformer"
 11
 12# Constants used to scale our sprites from their original size
 13CHARACTER_SCALING = 1
 14TILE_SCALING = 0.5
 15COIN_SCALING = 0.5
 16SPRITE_PIXEL_SIZE = 128
 17GRID_PIXEL_SIZE = (SPRITE_PIXEL_SIZE * TILE_SCALING)
 18
 19# Movement speed of player, in pixels per frame
 20PLAYER_MOVEMENT_SPEED = 7
 21GRAVITY = 1.5
 22PLAYER_JUMP_SPEED = 30
 23
 24# How many pixels to keep as a minimum margin between the character
 25# and the edge of the screen.
 26LEFT_VIEWPORT_MARGIN = 200
 27RIGHT_VIEWPORT_MARGIN = 200
 28BOTTOM_VIEWPORT_MARGIN = 150
 29TOP_VIEWPORT_MARGIN = 100
 30
 31PLAYER_START_X = 64
 32PLAYER_START_Y = 256
 33
 34
 35class MyGame(arcade.Window):
 36    """
 37    Main application class.
 38    """
 39
 40    def __init__(self):
 41        """
 42        Initializer for the game
 43        """
 44
 45        # Call the parent class and set up the window
 46        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 47
 48        # Set the path to start with this program
 49        file_path = os.path.dirname(os.path.abspath(__file__))
 50        os.chdir(file_path)
 51
 52        # These are 'lists' that keep track of our sprites. Each sprite should
 53        # go into a list.
 54        self.coin_list = None
 55        self.wall_list = None
 56        self.background_list = None
 57        self.ladder_list = None
 58        self.player_list = None
 59
 60        # Separate variable that holds the player sprite
 61        self.player_sprite = None
 62
 63        # Our 'physics' engine
 64        self.physics_engine = None
 65
 66        # Used to keep track of our scrolling
 67        self.view_bottom = 0
 68        self.view_left = 0
 69
 70        self.end_of_map = 0
 71
 72        # Keep track of the score
 73        self.score = 0
 74
 75        # Load sounds
 76        self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
 77        self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
 78        self.game_over = arcade.load_sound(":resources:sounds/gameover1.wav")
 79
 80    def setup(self):
 81        """ Set up the game here. Call this function to restart the game. """
 82
 83        # Used to keep track of our scrolling
 84        self.view_bottom = 0
 85        self.view_left = 0
 86
 87        # Keep track of the score
 88        self.score = 0
 89
 90        # Create the Sprite lists
 91        self.player_list = arcade.SpriteList()
 92        self.background_list = arcade.SpriteList()
 93        self.wall_list = arcade.SpriteList()
 94        self.coin_list = arcade.SpriteList()
 95
 96        # Set up the player, specifically placing it at these coordinates.
 97        image_source = ":resources:images/animated_characters/female_adventurer/femaleAdventurer_idle.png"
 98        self.player_sprite = arcade.Sprite(image_source, CHARACTER_SCALING)
 99        self.player_sprite.center_x = PLAYER_START_X
100        self.player_sprite.center_y = PLAYER_START_Y
101        self.player_list.append(self.player_sprite)
102
103        # --- Load in a map from the tiled editor ---
104
105        # Name of the layer in the file that has our platforms/walls
106        platforms_layer_name = 'Platforms'
107        moving_platforms_layer_name = 'Moving Platforms'
108
109        # Name of the layer that has items for pick-up
110        coins_layer_name = 'Coins'
111
112        # Map name
113        map_name = f":resources:tmx_maps/map_with_ladders.tmx"
114
115        # Read in the tiled map
116        my_map = arcade.tilemap.read_tmx(map_name)
117
118        # Calculate the right edge of the my_map in pixels
119        self.end_of_map = my_map.map_size.width * GRID_PIXEL_SIZE
120
121        # -- Platforms
122        self.wall_list = arcade.tilemap.process_layer(my_map,
123                                                      platforms_layer_name,
124                                                      scaling=TILE_SCALING,
125                                                      use_spatial_hash=True)
126
127        # -- Moving Platforms
128        moving_platforms_list = arcade.tilemap.process_layer(my_map, moving_platforms_layer_name, TILE_SCALING)
129        for sprite in moving_platforms_list:
130            self.wall_list.append(sprite)
131
132        # -- Background objects
133        self.background_list = arcade.tilemap.process_layer(my_map, "Background", TILE_SCALING)
134
135        # -- Background objects
136        self.ladder_list = arcade.tilemap.process_layer(my_map,
137                                                        "Ladders",
138                                                        scaling=TILE_SCALING,
139                                                        use_spatial_hash=True)
140
141        # -- Coins
142        self.coin_list = arcade.tilemap.process_layer(my_map,
143                                                      coins_layer_name,
144                                                      scaling=TILE_SCALING,
145                                                      use_spatial_hash=True)
146
147        # --- Other stuff
148        # Set the background color
149        if my_map.background_color:
150            arcade.set_background_color(my_map.background_color)
151
152        # Create the 'physics engine'
153        self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
154                                                             self.wall_list,
155                                                             gravity_constant=GRAVITY,
156                                                             ladders=self.ladder_list)
157
158    def on_draw(self):
159        """ Render the screen. """
160
161        # Clear the screen to the background color
162        arcade.start_render()
163
164        # Draw our sprites
165        self.wall_list.draw()
166        self.background_list.draw()
167        self.ladder_list.draw()
168        self.coin_list.draw()
169        self.player_list.draw()
170
171        # Draw our score on the screen, scrolling it with the viewport
172        score_text = f"Score: {self.score}"
173        arcade.draw_text(score_text, 10 + self.view_left, 10 + self.view_bottom,
174                         arcade.csscolor.BLACK, 18)
175
176    def on_key_press(self, key, modifiers):
177        """Called whenever a key is pressed. """
178
179        if key == arcade.key.UP or key == arcade.key.W:
180            if self.physics_engine.is_on_ladder():
181                self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
182            elif self.physics_engine.can_jump():
183                self.player_sprite.change_y = PLAYER_JUMP_SPEED
184                arcade.play_sound(self.jump_sound)
185        elif key == arcade.key.DOWN or key == arcade.key.S:
186            if self.physics_engine.is_on_ladder():
187                self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
188        elif key == arcade.key.LEFT or key == arcade.key.A:
189            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
190        elif key == arcade.key.RIGHT or key == arcade.key.D:
191            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
192
193    def on_key_release(self, key, modifiers):
194        """Called when the user releases a key. """
195
196        if key == arcade.key.UP or key == arcade.key.W:
197            if self.physics_engine.is_on_ladder():
198                self.player_sprite.change_y = 0
199        elif key == arcade.key.DOWN or key == arcade.key.S:
200            if self.physics_engine.is_on_ladder():
201                self.player_sprite.change_y = 0
202        elif key == arcade.key.LEFT or key == arcade.key.A:
203            self.player_sprite.change_x = 0
204        elif key == arcade.key.RIGHT or key == arcade.key.D:
205            self.player_sprite.change_x = 0
206
207    def update(self, delta_time):
208        """ Movement and game logic """
209
210        # Move the player with the physics engine
211        self.physics_engine.update()
212
213        # Update animations
214        self.coin_list.update_animation(delta_time)
215        self.background_list.update_animation(delta_time)
216
217        # Update walls, used with moving platforms
218        self.wall_list.update()
219
220        # See if the wall hit a boundary and needs to reverse direction.
221        for wall in self.wall_list:
222
223            if wall.boundary_right and wall.right > wall.boundary_right and wall.change_x > 0:
224                wall.change_x *= -1
225            if wall.boundary_left and wall.left < wall.boundary_left and wall.change_x < 0:
226                wall.change_x *= -1
227            if wall.boundary_top and wall.top > wall.boundary_top and wall.change_y > 0:
228                wall.change_y *= -1
229            if wall.boundary_bottom and wall.bottom < wall.boundary_bottom and wall.change_y < 0:
230                wall.change_y *= -1
231
232        # See if we hit any coins
233        coin_hit_list = arcade.check_for_collision_with_list(self.player_sprite,
234                                                             self.coin_list)
235
236        # Loop through each coin we hit (if any) and remove it
237        for coin in coin_hit_list:
238
239            # Figure out how many points this coin is worth
240            if 'Points' not in coin.properties:
241                print("Warning, collected a coing without a Points property.")
242            else:
243                points = int(coin.properties['Points'])
244                self.score += points
245
246            # Remove the coin
247            coin.remove_from_sprite_lists()
248            arcade.play_sound(self.collect_coin_sound)
249
250        # Track if we need to change the viewport
251        changed_viewport = False
252
253        # --- Manage Scrolling ---
254
255        # Scroll left
256        left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
257        if self.player_sprite.left < left_boundary:
258            self.view_left -= left_boundary - self.player_sprite.left
259            changed_viewport = True
260
261        # Scroll right
262        right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
263        if self.player_sprite.right > right_boundary:
264            self.view_left += self.player_sprite.right - right_boundary
265            changed_viewport = True
266
267        # Scroll up
268        top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
269        if self.player_sprite.top > top_boundary:
270            self.view_bottom += self.player_sprite.top - top_boundary
271            changed_viewport = True
272
273        # Scroll down
274        bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
275        if self.player_sprite.bottom < bottom_boundary:
276            self.view_bottom -= bottom_boundary - self.player_sprite.bottom
277            changed_viewport = True
278
279        if changed_viewport:
280            # Only scroll to integers. Otherwise we end up with pixels that
281            # don't line up on the screen
282            self.view_bottom = int(self.view_bottom)
283            self.view_left = int(self.view_left)
284
285            # Do the scrolling
286            arcade.set_viewport(self.view_left,
287                                SCREEN_WIDTH + self.view_left,
288                                self.view_bottom,
289                                SCREEN_HEIGHT + self.view_bottom)
290
291
292def main():
293    """ Main method """
294    window = MyGame()
295    window.setup()
296    arcade.run()
297
298
299if __name__ == "__main__":
300    main()

Step 11 - Add Character Animations, and Better Keyboard Control

Add character animations!

Animate Characters
  1"""
  2Platformer Game
  3
  4python -m arcade.examples.platform_tutorial.11_animate_character
  5"""
  6import arcade
  7import os
  8
  9# Constants
 10SCREEN_WIDTH = 1000
 11SCREEN_HEIGHT = 650
 12SCREEN_TITLE = "Platformer"
 13
 14# Constants used to scale our sprites from their original size
 15TILE_SCALING = 0.5
 16CHARACTER_SCALING = TILE_SCALING * 2
 17COIN_SCALING = TILE_SCALING
 18SPRITE_PIXEL_SIZE = 128
 19GRID_PIXEL_SIZE = (SPRITE_PIXEL_SIZE * TILE_SCALING)
 20
 21# Movement speed of player, in pixels per frame
 22PLAYER_MOVEMENT_SPEED = 7
 23GRAVITY = 1.5
 24PLAYER_JUMP_SPEED = 30
 25
 26# How many pixels to keep as a minimum margin between the character
 27# and the edge of the screen.
 28LEFT_VIEWPORT_MARGIN = 200
 29RIGHT_VIEWPORT_MARGIN = 200
 30BOTTOM_VIEWPORT_MARGIN = 150
 31TOP_VIEWPORT_MARGIN = 100
 32
 33PLAYER_START_X = SPRITE_PIXEL_SIZE * TILE_SCALING * 2
 34PLAYER_START_Y = SPRITE_PIXEL_SIZE * TILE_SCALING * 1
 35
 36# Constants used to track if the player is facing left or right
 37RIGHT_FACING = 0
 38LEFT_FACING = 1
 39
 40
 41def load_texture_pair(filename):
 42    """
 43    Load a texture pair, with the second being a mirror image.
 44    """
 45    return [
 46        arcade.load_texture(filename),
 47        arcade.load_texture(filename, flipped_horizontally=True)
 48    ]
 49
 50
 51class PlayerCharacter(arcade.Sprite):
 52    """ Player Sprite"""
 53    def __init__(self):
 54
 55        # Set up parent class
 56        super().__init__()
 57
 58        # Default to face-right
 59        self.character_face_direction = RIGHT_FACING
 60
 61        # Used for flipping between image sequences
 62        self.cur_texture = 0
 63        self.scale = CHARACTER_SCALING
 64
 65        # Track our state
 66        self.jumping = False
 67        self.climbing = False
 68        self.is_on_ladder = False
 69
 70        # --- Load Textures ---
 71
 72        # Images from Kenney.nl's Asset Pack 3
 73        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
 74        # main_path = ":resources:images/animated_characters/female_person/femalePerson"
 75        main_path = ":resources:images/animated_characters/male_person/malePerson"
 76        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
 77        # main_path = ":resources:images/animated_characters/zombie/zombie"
 78        # main_path = ":resources:images/animated_characters/robot/robot"
 79
 80        # Load textures for idle standing
 81        self.idle_texture_pair = load_texture_pair(f"{main_path}_idle.png")
 82        self.jump_texture_pair = load_texture_pair(f"{main_path}_jump.png")
 83        self.fall_texture_pair = load_texture_pair(f"{main_path}_fall.png")
 84
 85        # Load textures for walking
 86        self.walk_textures = []
 87        for i in range(8):
 88            texture = load_texture_pair(f"{main_path}_walk{i}.png")
 89            self.walk_textures.append(texture)
 90
 91        # Load textures for climbing
 92        self.climbing_textures = []
 93        texture = arcade.load_texture(f"{main_path}_climb0.png")
 94        self.climbing_textures.append(texture)
 95        texture = arcade.load_texture(f"{main_path}_climb1.png")
 96        self.climbing_textures.append(texture)
 97
 98        # Set the initial texture
 99        self.texture = self.idle_texture_pair[0]
100
101        # Hit box will be set based on the first image used. If you want to specify
102        # a different hit box, you can do it like the code below.
103        # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]])
104        self.set_hit_box(self.texture.hit_box_points)
105
106    def update_animation(self, delta_time: float = 1/60):
107
108        # Figure out if we need to flip face left or right
109        if self.change_x < 0 and self.character_face_direction == RIGHT_FACING:
110            self.character_face_direction = LEFT_FACING
111        elif self.change_x > 0 and self.character_face_direction == LEFT_FACING:
112            self.character_face_direction = RIGHT_FACING
113
114        # Climbing animation
115        if self.is_on_ladder:
116            self.climbing = True
117        if not self.is_on_ladder and self.climbing:
118            self.climbing = False
119        if self.climbing and abs(self.change_y) > 1:
120            self.cur_texture += 1
121            if self.cur_texture > 7:
122                self.cur_texture = 0
123        if self.climbing:
124            self.texture = self.climbing_textures[self.cur_texture // 4]
125            return
126
127        # Jumping animation
128        if self.change_y > 0 and not self.is_on_ladder:
129            self.texture = self.jump_texture_pair[self.character_face_direction]
130            return
131        elif self.change_y < 0 and not self.is_on_ladder:
132            self.texture = self.fall_texture_pair[self.character_face_direction]
133            return
134
135        # Idle animation
136        if self.change_x == 0:
137            self.texture = self.idle_texture_pair[self.character_face_direction]
138            return
139
140        # Walking animation
141        self.cur_texture += 1
142        if self.cur_texture > 7:
143            self.cur_texture = 0
144        self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]
145
146
147class MyGame(arcade.Window):
148    """
149    Main application class.
150    """
151
152    def __init__(self):
153        """
154        Initializer for the game
155        """
156
157        # Call the parent class and set up the window
158        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
159
160        # Set the path to start with this program
161        file_path = os.path.dirname(os.path.abspath(__file__))
162        os.chdir(file_path)
163
164        # Track the current state of what key is pressed
165        self.left_pressed = False
166        self.right_pressed = False
167        self.up_pressed = False
168        self.down_pressed = False
169        self.jump_needs_reset = False
170
171        # These are 'lists' that keep track of our sprites. Each sprite should
172        # go into a list.
173        self.coin_list = None
174        self.wall_list = None
175        self.background_list = None
176        self.ladder_list = None
177        self.player_list = None
178
179        # Separate variable that holds the player sprite
180        self.player_sprite = None
181
182        # Our 'physics' engine
183        self.physics_engine = None
184
185        # Used to keep track of our scrolling
186        self.view_bottom = 0
187        self.view_left = 0
188
189        self.end_of_map = 0
190
191        # Keep track of the score
192        self.score = 0
193
194        # Load sounds
195        self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
196        self.jump_sound = arcade.load_sound(":resources:sounds/jump1.wav")
197        self.game_over = arcade.load_sound(":resources:sounds/gameover1.wav")
198
199    def setup(self):
200        """ Set up the game here. Call this function to restart the game. """
201
202        # Used to keep track of our scrolling
203        self.view_bottom = 0
204        self.view_left = 0
205
206        # Keep track of the score
207        self.score = 0
208
209        # Create the Sprite lists
210        self.player_list = arcade.SpriteList()
211        self.background_list = arcade.SpriteList()
212        self.wall_list = arcade.SpriteList()
213        self.coin_list = arcade.SpriteList()
214
215        # Set up the player, specifically placing it at these coordinates.
216        self.player_sprite = PlayerCharacter()
217
218        self.player_sprite.center_x = PLAYER_START_X
219        self.player_sprite.center_y = PLAYER_START_Y
220        self.player_list.append(self.player_sprite)
221
222        # --- Load in a map from the tiled editor ---
223
224        # Name of the layer in the file that has our platforms/walls
225        platforms_layer_name = 'Platforms'
226        moving_platforms_layer_name = 'Moving Platforms'
227
228        # Name of the layer that has items for pick-up
229        coins_layer_name = 'Coins'
230
231        # Map name
232        map_name = f":resources:tmx_maps/map_with_ladders.tmx"
233
234        # Read in the tiled map
235        my_map = arcade.tilemap.read_tmx(map_name)
236
237        # Calculate the right edge of the my_map in pixels
238        self.end_of_map = my_map.map_size.width * GRID_PIXEL_SIZE
239
240        # -- Platforms
241        self.wall_list = arcade.tilemap.process_layer(my_map,
242                                                      platforms_layer_name,
243                                                      TILE_SCALING,
244                                                      use_spatial_hash=True)
245
246        # -- Moving Platforms
247        moving_platforms_list = arcade.tilemap.process_layer(my_map, moving_platforms_layer_name, TILE_SCALING)
248        for sprite in moving_platforms_list:
249            self.wall_list.append(sprite)
250
251        # -- Background objects
252        self.background_list = arcade.tilemap.process_layer(my_map, "Background", TILE_SCALING)
253
254        # -- Background objects
255        self.ladder_list = arcade.tilemap.process_layer(my_map, "Ladders",
256                                                        TILE_SCALING,
257                                                        use_spatial_hash=True)
258
259        # -- Coins
260        self.coin_list = arcade.tilemap.process_layer(my_map, coins_layer_name,
261                                                      TILE_SCALING,
262                                                      use_spatial_hash=True)
263
264        # --- Other stuff
265        # Set the background color
266        if my_map.background_color:
267            arcade.set_background_color(my_map.background_color)
268
269        # Create the 'physics engine'
270        self.physics_engine = arcade.PhysicsEnginePlatformer(self.player_sprite,
271                                                             self.wall_list,
272                                                             gravity_constant=GRAVITY,
273                                                             ladders=self.ladder_list)
274
275    def on_draw(self):
276        """ Render the screen. """
277
278        # Clear the screen to the background color
279        arcade.start_render()
280
281        # Draw our sprites
282        self.wall_list.draw()
283        self.background_list.draw()
284        self.ladder_list.draw()
285        self.coin_list.draw()
286        self.player_list.draw()
287
288        # Draw our score on the screen, scrolling it with the viewport
289        score_text = f"Score: {self.score}"
290        arcade.draw_text(score_text, 10 + self.view_left, 10 + self.view_bottom,
291                         arcade.csscolor.BLACK, 18)
292
293        # Draw hit boxes.
294        # for wall in self.wall_list:
295        #     wall.draw_hit_box(arcade.color.BLACK, 3)
296        #
297        # self.player_sprite.draw_hit_box(arcade.color.RED, 3)
298
299    def process_keychange(self):
300        """
301        Called when we change a key up/down or we move on/off a ladder.
302        """
303        # Process up/down
304        if self.up_pressed and not self.down_pressed:
305            if self.physics_engine.is_on_ladder():
306                self.player_sprite.change_y = PLAYER_MOVEMENT_SPEED
307            elif self.physics_engine.can_jump(y_distance=10) and not self.jump_needs_reset:
308                self.player_sprite.change_y = PLAYER_JUMP_SPEED
309                self.jump_needs_reset = True
310                arcade.play_sound(self.jump_sound)
311        elif self.down_pressed and not self.up_pressed:
312            if self.physics_engine.is_on_ladder():
313                self.player_sprite.change_y = -PLAYER_MOVEMENT_SPEED
314
315        # Process up/down when on a ladder and no movement
316        if self.physics_engine.is_on_ladder():
317            if not self.up_pressed and not self.down_pressed:
318                self.player_sprite.change_y = 0
319            elif self.up_pressed and self.down_pressed:
320                self.player_sprite.change_y = 0
321
322        # Process left/right
323        if self.right_pressed and not self.left_pressed:
324            self.player_sprite.change_x = PLAYER_MOVEMENT_SPEED
325        elif self.left_pressed and not self.right_pressed:
326            self.player_sprite.change_x = -PLAYER_MOVEMENT_SPEED
327        else:
328            self.player_sprite.change_x = 0
329
330    def on_key_press(self, key, modifiers):
331        """Called whenever a key is pressed. """
332
333        if key == arcade.key.UP or key == arcade.key.W:
334            self.up_pressed = True
335        elif key == arcade.key.DOWN or key == arcade.key.S:
336            self.down_pressed = True
337        elif key == arcade.key.LEFT or key == arcade.key.A:
338            self.left_pressed = True
339        elif key == arcade.key.RIGHT or key == arcade.key.D:
340            self.right_pressed = True
341
342        self.process_keychange()
343
344    def on_key_release(self, key, modifiers):
345        """Called when the user releases a key. """
346
347        if key == arcade.key.UP or key == arcade.key.W:
348            self.up_pressed = False
349            self.jump_needs_reset = False
350        elif key == arcade.key.DOWN or key == arcade.key.S:
351            self.down_pressed = False
352        elif key == arcade.key.LEFT or key == arcade.key.A:
353            self.left_pressed = False
354        elif key == arcade.key.RIGHT or key == arcade.key.D:
355            self.right_pressed = False
356
357        self.process_keychange()
358
359    def on_update(self, delta_time):
360        """ Movement and game logic """
361
362        # Move the player with the physics engine
363        self.physics_engine.update()
364
365        # Update animations
366        if self.physics_engine.can_jump():
367            self.player_sprite.can_jump = False
368        else:
369            self.player_sprite.can_jump = True
370
371        if self.physics_engine.is_on_ladder() and not self.physics_engine.can_jump():
372            self.player_sprite.is_on_ladder = True
373            self.process_keychange()
374        else:
375            self.player_sprite.is_on_ladder = False
376            self.process_keychange()
377
378        self.coin_list.update_animation(delta_time)
379        self.background_list.update_animation(delta_time)
380        self.player_list.update_animation(delta_time)
381
382        # Update walls, used with moving platforms
383        self.wall_list.update()
384
385        # See if the moving wall hit a boundary and needs to reverse direction.
386        for wall in self.wall_list:
387
388            if wall.boundary_right and wall.right > wall.boundary_right and wall.change_x > 0:
389                wall.change_x *= -1
390            if wall.boundary_left and wall.left < wall.boundary_left and wall.change_x < 0:
391                wall.change_x *= -1
392            if wall.boundary_top and wall.top > wall.boundary_top and wall.change_y > 0:
393                wall.change_y *= -1
394            if wall.boundary_bottom and wall.bottom < wall.boundary_bottom and wall.change_y < 0:
395                wall.change_y *= -1
396
397        # See if we hit any coins
398        coin_hit_list = arcade.check_for_collision_with_list(self.player_sprite,
399                                                             self.coin_list)
400
401        # Loop through each coin we hit (if any) and remove it
402        for coin in coin_hit_list:
403
404            # Figure out how many points this coin is worth
405            if 'Points' not in coin.properties:
406                print("Warning, collected a coin without a Points property.")
407            else:
408                points = int(coin.properties['Points'])
409                self.score += points
410
411            # Remove the coin
412            coin.remove_from_sprite_lists()
413            arcade.play_sound(self.collect_coin_sound)
414
415        # Track if we need to change the viewport
416        changed_viewport = False
417
418        # --- Manage Scrolling ---
419
420        # Scroll left
421        left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
422        if self.player_sprite.left < left_boundary:
423            self.view_left -= left_boundary - self.player_sprite.left
424            changed_viewport = True
425
426        # Scroll right
427        right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
428        if self.player_sprite.right > right_boundary:
429            self.view_left += self.player_sprite.right - right_boundary
430            changed_viewport = True
431
432        # Scroll up
433        top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
434        if self.player_sprite.top > top_boundary:
435            self.view_bottom += self.player_sprite.top - top_boundary
436            changed_viewport = True
437
438        # Scroll down
439        bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
440        if self.player_sprite.bottom < bottom_boundary:
441            self.view_bottom -= bottom_boundary - self.player_sprite.bottom
442            changed_viewport = True
443
444        if changed_viewport:
445            # Only scroll to integers. Otherwise we end up with pixels that
446            # don't line up on the screen
447            self.view_bottom = int(self.view_bottom)
448            self.view_left = int(self.view_left)
449
450            # Do the scrolling
451            arcade.set_viewport(self.view_left,
452                                SCREEN_WIDTH + self.view_left,
453                                self.view_bottom,
454                                SCREEN_HEIGHT + self.view_bottom)
455
456
457def main():
458    """ Main method """
459    window = MyGame()
460    window.setup()
461    arcade.run()
462
463
464if __name__ == "__main__":
465    main()

Pymunk Platformer

_images/title_animated_gif.gif

This tutorial covers how to write a platformer using Arcade and its Pymunk API. This tutorial assumes the you are somewhat familiar with Python, Arcade, and the Tiled Map Editor.

  • If you aren’t familiar with programming in Python, check out https://learn.arcade.academy

  • If you aren’t familiar with the Arcade library, work through the Simple Platformer.

  • If you aren’t familiar with the Tiled Map Editor, the Simple Platformer also introduces how to create a map with the Tiled Map Editor.

Common Issues

There are a few items with the Pymunk physics engine that should be pointed out before you get started:

  • Object overlap - A fast moving object is allowed to overlap with the object it collides with, and Pymunk will push them apart later. See collision bias for more information.

  • Pass-through - A fast moving object can pass through another object if its speed is so quick it never overlaps the other object between frames. See object tunneling.

  • When stepping the physics engine forward in time, the default is to move forward 1/60th of a second. Whatever increment is picked, increments should always be kept the same. Don’t use the variable delta_time from the update method as a unit, or results will be unstable and unpredictable. For a more accurate simulation, you can step forward 1/120th of a second twice per frame. This increases the time required, but takes more time to calculate.

  • A sprite moving across a floor made up of many rectangles can get “caught” on the edges. The corner of the player sprite can get caught the corner of the floor sprite. To get around this, make sure the hit box for the bottom of the player sprite is rounded. Also, look into the possibility of merging horizontal rows of sprites.

Open a Window

To begin with, let’s start with a program that will use Arcade to open a blank window. It also has stubs for methods we’ll fill in later. Try this code and make sure you can run it. It should pop open a black window.

Starting Program
 1"""
 2Example of Pymunk Physics Engine Platformer
 3"""
 4import arcade
 5
 6SCREEN_TITLE = "PyMunk Platformer"
 7
 8# Size of screen to show, in pixels
 9SCREEN_WIDTH = 800
10SCREEN_HEIGHT = 600
11
12
13class GameWindow(arcade.Window):
14    """ Main Window """
15
16    def __init__(self, width, height, title):
17        """ Create the variables """
18
19        # Init the parent class
20        super().__init__(width, height, title)
21
22    def setup(self):
23        """ Set up everything with the game """
24        pass
25
26    def on_key_press(self, key, modifiers):
27        """Called whenever a key is pressed. """
28        pass
29
30    def on_key_release(self, key, modifiers):
31        """Called when the user releases a key. """
32        pass
33
34    def on_update(self, delta_time):
35        """ Movement and game logic """
36        pass
37
38    def on_draw(self):
39        """ Draw everything """
40        arcade.start_render()
41
42def main():
43    """ Main method """
44    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
45    window.setup()
46    arcade.run()
47
48
49if __name__ == "__main__":
50    main()

Create Constants

Now let’s set up the import statements, and define the constants we are going to use. In this case, we’ve got sprite tiles that are 128x128 pixels. They are scaled down to 50% of the width and 50% of the height (scale of 0.5). The screen size is set to 25x15 grid.

To keep things simple, this example will not scroll the screen with the player. See Simple Platformer or Move with a Scrolling Screen.

When you run this program, the screen should be larger.

Adding some constants
 1"""
 2Example of Pymunk Physics Engine Platformer
 3"""
 4import math
 5from typing import Optional
 6import arcade
 7
 8SCREEN_TITLE = "PyMunk Platformer"
 9
10# How big are our image tiles?
11SPRITE_IMAGE_SIZE = 128
12
13# Scale sprites up or down
14SPRITE_SCALING_PLAYER = 0.5
15SPRITE_SCALING_TILES = 0.5
16
17# Scaled sprite size for tiles
18SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)
19
20# Size of grid to show on screen, in number of tiles
21SCREEN_GRID_WIDTH = 25
22SCREEN_GRID_HEIGHT = 15
23
24# Size of screen to show, in pixels
25SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
26SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT
27
28
29class GameWindow(arcade.Window):

Create Instance Variables

Next, let’s create instance variables we are going to use, and set a background color that’s green: arcade.color.AMAZON

If you aren’t familiar with type-casting on Python, you might not be familiar with lines of code like this:

self.player_list: Optional[arcade.SpriteList] = None

This means the player_list attribute is going to be an instance of SpriteList or None. If you don’t want to mess with typing, then this code also works just as well:

self.player_list = None

Running this program should show the same window, but with a green background.

Create instance variables
 1class GameWindow(arcade.Window):
 2    """ Main Window """
 3
 4    def __init__(self, width, height, title):
 5        """ Create the variables """
 6
 7        # Init the parent class
 8        super().__init__(width, height, title)
 9
10        # Player sprite
11        self.player_sprite: Optional[arcade.Sprite] = None
12
13        # Sprite lists we need
14        self.player_list: Optional[arcade.SpriteList] = None
15        self.wall_list: Optional[arcade.SpriteList] = None
16        self.bullet_list: Optional[arcade.SpriteList] = None
17        self.item_list: Optional[arcade.SpriteList] = None
18
19        # Track the current state of what key is pressed
20        self.left_pressed: bool = False
21        self.right_pressed: bool = False
22
23        # Set background color
24        arcade.set_background_color(arcade.color.AMAZON)

Load and Display Map

To get started, create a map with the Tiled Map Editor. Place items that you don’t want to move, and to act as platforms in a layer named “Platforms”. Place items you want to push around in a layer called “Dynamic Items”. Name the file “pymunk_test_map.tmx” and place in the exact same directory as your code.

_images/tiled_map.png

If you aren’t sure how to use the Tiled Map Editor, see Step 8 - Use a Map Editor.

Now, in the setup function, we are going add code to:

  • Create instances of SpriteList for each group of sprites we are doing to work with.

  • Create the player sprite.

  • Read in the tiled map.

  • Make sprites from the layers in the tiled map.

Note

When making sprites from the tiled map layer, the name of the layer you load must match exactly with the layer created in the tiled map editor. It is case-sensitive.

Creating our sprites
 1    def setup(self):
 2        """ Set up everything with the game """
 3
 4        # Create the sprite lists
 5        self.player_list = arcade.SpriteList()
 6        self.bullet_list = arcade.SpriteList()
 7
 8        # Read in the tiled map
 9        map_name = "pymunk_test_map.tmx"
10        my_map = arcade.tilemap.read_tmx(map_name)
11
12        # Read in the map layers
13        self.wall_list = arcade.tilemap.process_layer(my_map, 'Platforms', SPRITE_SCALING_TILES)
14        self.item_list = arcade.tilemap.process_layer(my_map, 'Dynamic Items', SPRITE_SCALING_TILES)
15
16        # Create player sprite
17        self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png",
18                                           SPRITE_SCALING_PLAYER)
19        # Set player location
20        grid_x = 1
21        grid_y = 1
22        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
23        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2
24        # Add to player sprite list
25        self.player_list.append(self.player_sprite)

There’s no point in having sprites if we don’t draw them, so in the on_draw method, let’s draw out sprite lists.

Drawing our sprites
1    def on_draw(self):
2        """ Draw everything """
3        arcade.start_render()
4        self.wall_list.draw()
5        self.bullet_list.draw()
6        self.item_list.draw()
7        self.player_list.draw()

With the additions in the program below, running your program should show the tiled map you created:

_images/pymunk_demo_platformer_04.png

Add Physics Engine

The next step is to add in the physics engine.

First, add some constants for our physics. Here we are setting:

  • A constant for the force of gravity

  • Values for “damping”. A damping of 1.0 will cause an item to lose all it’s velocity once a force no longer applies to it. A damping of 0.5 causes 50% of speed to be lost in 1 second. A value of 0 is free-fall.

  • Values for friction. 0.0 is ice, 1.0 is like rubber.

  • Mass. Item default to 1. We make the player 2, so she can push items around easier.

  • Limits are the players horizontal and vertical speed. It is easier to play if the player is limited to a constant speed. And more realistic, because they aren’t on wheels.

Add Constants for Physics
 1# --- Physics forces. Higher number, faster accelerating.
 2
 3# Gravity
 4GRAVITY = 1500
 5
 6# Damping - Amount of speed lost per second
 7DEFAULT_DAMPING = 1.0
 8PLAYER_DAMPING = 0.4
 9
10# Friction between objects
11PLAYER_FRICTION = 1.0
12WALL_FRICTION = 0.7
13DYNAMIC_ITEM_FRICTION = 0.6
14
15# Mass (defaults to 1)
16PLAYER_MASS = 2.0
17
18# Keep player from going too fast
19PLAYER_MAX_HORIZONTAL_SPEED = 450
20PLAYER_MAX_VERTICAL_SPEED = 1600

Second, add the following attributer in the __init__ method to hold our physics engine:

Add Physics Engine Attribute
1        # Physics engine
2        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

Third, in the setup method we create the physics engine and add the sprites. The player, walls, and dynamic items all have different properties so they are added individually.

Add Sprites to Physics Engine in ‘setup’ Method
 1        # --- Pymunk Physics Engine Setup ---
 2
 3        # The default damping for every object controls the percent of velocity
 4        # the object will keep each second. A value of 1.0 is no speed loss,
 5        # 0.9 is 10% per second, 0.1 is 90% per second.
 6        # For top-down games, this is basically the friction for moving objects.
 7        # For platformers with gravity, this should probably be set to 1.0.
 8        # Default value is 1.0 if not specified.
 9        damping = DEFAULT_DAMPING
10
11        # Set the gravity. (0, 0) is good for outer space and top-down.
12        gravity = (0, -GRAVITY)
13
14        # Create the physics engine
15        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
16                                                         gravity=gravity)
17
18        # Add the player.
19        # For the player, we set the damping to a lower value, which increases
20        # the damping rate. This prevents the character from traveling too far
21        # after the player lets off the movement keys.
22        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
23        # rotating.
24        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
25        # Friction is between two objects in contact. It is important to remember
26        # in top-down games that friction moving along the 'floor' is controlled
27        # by damping.
28        self.physics_engine.add_sprite(self.player_sprite,
29                                       friction=PLAYER_FRICTION,
30                                       mass=PLAYER_MASS,
31                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
32                                       collision_type="player",
33                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
34                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)
35
36        # Create the walls.
37        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
38        # move.
39        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
40        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
41        # repositioned by code and don't respond to physics forces.
42        # Dynamic is default.
43        self.physics_engine.add_sprite_list(self.wall_list,
44                                            friction=WALL_FRICTION,
45                                            collision_type="wall",
46                                            body_type=arcade.PymunkPhysicsEngine.STATIC)
47
48        # Create the items
49        self.physics_engine.add_sprite_list(self.item_list,
50                                            friction=DYNAMIC_ITEM_FRICTION,
51                                            collision_type="item")

Fourth, in the on_update method we call the physics engine’s step method.

Add Sprites to Physics Engine in ‘setup’ Method
1    def on_update(self, delta_time):
2        """ Movement and game logic """
3        self.physics_engine.step()

If you run the program, and you have dynamic items that are up in the air, you should see them fall when the game starts.

Add Player Movement

Next step is to get the player moving. In this section we’ll cover how to move left and right. In the next section we’ll show how to jump.

The force that we will move the player is defined as PLAYER_MOVE_FORCE_ON_GROUND. We’ll apply a different force later, if the player happens to be airborne.

Add Player Movement - Constants and Attributes
 1# Force applied while on the ground
 2PLAYER_MOVE_FORCE_ON_GROUND = 8000
 3
 4class GameWindow(arcade.Window):
 5    """ Main Window """
 6
 7    def __init__(self, width, height, title):
 8        """ Create the variables """
 9
10        # Init the parent class
11        super().__init__(width, height, title)
12
13        # Player sprite
14        self.player_sprite: Optional[arcade.Sprite] = None
15
16        # Sprite lists we need
17        self.player_list: Optional[arcade.SpriteList] = None
18        self.wall_list: Optional[arcade.SpriteList] = None
19        self.bullet_list: Optional[arcade.SpriteList] = None
20        self.item_list: Optional[arcade.SpriteList] = None
21
22        # Track the current state of what key is pressed
23        self.left_pressed: bool = False
24        self.right_pressed: bool = False

We need to track if the left/right keys are held down. To do this we define instance variables left_pressed and right_pressed. These are set to appropriate values in the key press and release handlers.

Handle Key Up and Down Events
 1    def on_key_press(self, key, modifiers):
 2        """Called whenever a key is pressed. """
 3
 4        if key == arcade.key.LEFT:
 5            self.left_pressed = True
 6        elif key == arcade.key.RIGHT:
 7            self.right_pressed = True
 8
 9    def on_key_release(self, key, modifiers):
10        """Called when the user releases a key. """
11
12        if key == arcade.key.LEFT:
13            self.left_pressed = False
14        elif key == arcade.key.RIGHT:
15            self.right_pressed = False

Finally, we need to apply the correct force in on_update. Force is specified in a tuple with horizontal force first, and vertical force second.

We also set the friction when we are moving to zero, and when we are not moving to 1. This is important to get realistic movement.

Apply Force to Move Player
 1    def on_update(self, delta_time):
 2        """ Movement and game logic """
 3
 4        # Update player forces based on keys pressed
 5        if self.left_pressed and not self.right_pressed:
 6            # Create a force to the left. Apply it.
 7            force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
 8            self.physics_engine.apply_force(self.player_sprite, force)
 9            # Set friction to zero for the player while moving
10            self.physics_engine.set_friction(self.player_sprite, 0)
11        elif self.right_pressed and not self.left_pressed:
12            # Create a force to the right. Apply it.
13            force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
14            self.physics_engine.apply_force(self.player_sprite, force)
15            # Set friction to zero for the player while moving
16            self.physics_engine.set_friction(self.player_sprite, 0)
17        else:
18            # Player's feet are not moving. Therefore up the friction so we stop.
19            self.physics_engine.set_friction(self.player_sprite, 1.0)
20
21        # Move items in the physics engine
22        self.physics_engine.step()

Add Player Jumping

To get the player to jump we need to:

  • Make sure the player is on the ground

  • Apply an impulse force to the player upward

  • Change the left/right force to the player while they are in the air.

We can see if a sprite has a sprite below it with the is_on_ground function. Otherwise we’ll be able to jump while we are in the air. (Double-jumps would allow this once.)

If we don’t allow the player to move left-right while in the air, they player will be very hard to control. If we allow them to move left/right with the same force as on the ground, that’s typically too much. So we’ve got a different left/right force depending if we are in the air or not.

For the code changes, first we’ll define some constants:

Add Player Jumping - Constants
1# Force applied when moving left/right in the air
2PLAYER_MOVE_FORCE_IN_AIR = 900
3
4# Strength of a jump
5PLAYER_JUMP_IMPULSE = 1800

We’ll add logic that will apply the impulse force when we jump:

Add Player Jumping - Jump Force
 1    def on_key_press(self, key, modifiers):
 2        """Called whenever a key is pressed. """
 3
 4        if key == arcade.key.LEFT:
 5            self.left_pressed = True
 6        elif key == arcade.key.RIGHT:
 7            self.right_pressed = True
 8        elif key == arcade.key.UP:
 9            # find out if player is standing on ground
10            if self.physics_engine.is_on_ground(self.player_sprite):
11                # She is! Go ahead and jump
12                impulse = (0, PLAYER_JUMP_IMPULSE)
13                self.physics_engine.apply_impulse(self.player_sprite, impulse)

Then we will adjust the left/right force depending on if we are grounded or not:

Add Player Jumping - Left/Right Force Selection
 1        """ Movement and game logic """
 2
 3        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
 4        # Update player forces based on keys pressed
 5        if self.left_pressed and not self.right_pressed:
 6            # Create a force to the left. Apply it.
 7            if is_on_ground:
 8                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
 9            else:
10                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)
11            self.physics_engine.apply_force(self.player_sprite, force)
12            # Set friction to zero for the player while moving
13            self.physics_engine.set_friction(self.player_sprite, 0)
14        elif self.right_pressed and not self.left_pressed:
15            # Create a force to the right. Apply it.
16            if is_on_ground:
17                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
18            else:
19                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)
20            self.physics_engine.apply_force(self.player_sprite, force)
21            # Set friction to zero for the player while moving
22            self.physics_engine.set_friction(self.player_sprite, 0)
23        else:
24            # Player's feet are not moving. Therefore up the friction so we stop.
25            self.physics_engine.set_friction(self.player_sprite, 1.0)
26
27        # Move items in the physics engine

Add Player Animation

To create a player animation, we make a custom child class of Sprite. We load each frame of animation that we need, including a mirror image of it.

We will flip the player to face left or right. If the player is in the air, we’ll also change between a jump up and a falling graphics.

Because the physics engine works with small floating point numbers, it often flips above and below zero by small amounts. It is a good idea not to change the animation as the x and y float around zero. For that reason, in this code we have a “dead zone.” We don’t change the animation until it gets outside of that zone.

We also need to control how far the player moves before we change the walking animation, so that the feet appear in-sync with the ground.

Add Player Animation - Constants
1# Close enough to not-moving to have the animation go to idle.
2DEAD_ZONE = 0.1
3
4# Constants used to track if the player is facing left or right
5RIGHT_FACING = 0
6LEFT_FACING = 1
7
8# How many pixels to move before we change the texture in the walking animation
9DISTANCE_TO_CHANGE_TEXTURE = 20

Next, we create a Player class that is a child to arcade.Sprite. This class will update the player animation.

The __init__ method loads all of the textures. Here we use Kenney.nl’s Toon Characters 1 pack. It has six different characters you can choose from with the same layout, so it makes changing as simple as changing which line is enabled. There are eight textures for walking, and textures for idle, jumping, and falling.

As the character can face left or right, we use arcade.load_texture_pair which will load both a regular image, and one that’s mirrored.

For the multi-frame walking animation, we use an “odometer.” We need to move a certain number of pixels before changing the animation. If this value is too small our character moves her legs like Fred Flintstone, too large and it looks like you are ice skating. We keep track of the index of our current texture, 0-7 since there are eight of them.

Any sprite moved by the Pymunk engine will have its pymunk_moved method called. This can be used to update the animation.

Add Player Animation - Player Class
 1class PlayerSprite(arcade.Sprite):
 2    """ Player Sprite """
 3    def __init__(self):
 4        """ Init """
 5        # Let parent initialize
 6        super().__init__()
 7
 8        # Set our scale
 9        self.scale = SPRITE_SCALING_PLAYER
10
11        # Images from Kenney.nl's Character pack
12        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
13        main_path = ":resources:images/animated_characters/female_person/femalePerson"
14        # main_path = ":resources:images/animated_characters/male_person/malePerson"
15        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
16        # main_path = ":resources:images/animated_characters/zombie/zombie"
17        # main_path = ":resources:images/animated_characters/robot/robot"
18
19        # Load textures for idle standing
20        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png")
21        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")
22        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")
23
24        # Load textures for walking
25        self.walk_textures = []
26        for i in range(8):
27            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
28            self.walk_textures.append(texture)
29
30        # Set the initial texture
31        self.texture = self.idle_texture_pair[0]
32
33        # Hit box will be set based on the first image used.
34        self.hit_box = self.texture.hit_box_points
35
36        # Default to face-right
37        self.character_face_direction = RIGHT_FACING
38
39        # Index of our current texture
40        self.cur_texture = 0
41
42        # How far have we traveled horizontally since changing the texture
43        self.x_odometer = 0
44
45    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
46        """ Handle being moved by the pymunk engine """
47        # Figure out if we need to face left or right
48        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
49            self.character_face_direction = LEFT_FACING
50        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
51            self.character_face_direction = RIGHT_FACING
52
53        # Are we on the ground?
54        is_on_ground = physics_engine.is_on_ground(self)
55
56        # Add to the odometer how far we've moved
57        self.x_odometer += dx
58
59        # Jumping animation
60        if not is_on_ground:
61            if dy > DEAD_ZONE:
62                self.texture = self.jump_texture_pair[self.character_face_direction]
63                return
64            elif dy < -DEAD_ZONE:
65                self.texture = self.fall_texture_pair[self.character_face_direction]
66                return
67
68        # Idle animation
69        if abs(dx) <= DEAD_ZONE:
70            self.texture = self.idle_texture_pair[self.character_face_direction]
71            return
72
73        # Have we moved far enough to change the texture?
74        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:
75
76            # Reset the odometer
77            self.x_odometer = 0
78
79            # Advance the walking animation
80            self.cur_texture += 1
81            if self.cur_texture > 7:
82                self.cur_texture = 0
83            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]

Important! At this point, we are still creating an instance of arcade.Sprite and not PlayerSprite. We need to go back to the setup method and replace the line that creates the player instance with:

Add Player Animation - Creating the Player Class
        # Create player sprite
        self.player_sprite = PlayerSprite()

A really common mistake I’ve seen programmers make (and made myself) is to forget that last part. Then you can spend a lot of time looking at the player class when the error is in the setup.

We also need to go back and change the data type for the player sprite attribute in our __init__ method:

Add Player Animation - Creating the Player Class
        # Player sprite
        self.player_sprite: Optional[PlayerSprite] = None

Shoot Bullets

Getting the player to shoot something can add a lot to our game. To begin with we’ll define a few constants to use. How much force to shoot the bullet with, the bullet’s mass, and the gravity to use for the bullet.

If we use the same gravity for the bullet as everything else, it tends to drop too fast. We could set this to zero if we wanted it to not drop at all.

Shoot Bullets - Constants
1# How much force to put on the bullet
2BULLET_MOVE_FORCE = 4500
3
4# Mass of the bullet
5BULLET_MASS = 0.1
6
7# Make bullet less affected by gravity
8BULLET_GRAVITY = 300

Next, we’ll put in a mouse press handler to put in the bullet shooting code.

We need to:

  • Create the bullet sprite

  • We need to calculate the angle from the player to the mouse click

  • Create the bullet away from the player in the proper direction, as spawning it inside the player will confuse the physics engine

  • Add the bullet to the physics engine

  • Apply the force to the bullet to make if move. Note that as we angled the bullet we don’t need to angle the force.

Warning

Does your platformer scroll?

If your window scrolls, you need to add in the coordinate off-set or else the angle calculation will be incorrect.

Warning

Bullets don’t disappear yet!

If the bullet flies off-screen, it doesn’t go away and the physics engine still has to track it.

Shoot Bullets - Mouse Press
 1    def on_mouse_press(self, x, y, button, modifiers):
 2        """ Called whenever the mouse button is clicked. """
 3
 4        bullet = arcade.SpriteSolidColor(20, 5, arcade.color.DARK_YELLOW)
 5        self.bullet_list.append(bullet)
 6
 7        # Position the bullet at the player's current location
 8        start_x = self.player_sprite.center_x
 9        start_y = self.player_sprite.center_y
10        bullet.position = self.player_sprite.position
11
12        # Get from the mouse the destination location for the bullet
13        # IMPORTANT! If you have a scrolling screen, you will also need
14        # to add in self.view_bottom and self.view_left.
15        dest_x = x
16        dest_y = y
17
18        # Do math to calculate how to get the bullet to the destination.
19        # Calculation the angle in radians between the start points
20        # and end points. This is the angle the bullet will travel.
21        x_diff = dest_x - start_x
22        y_diff = dest_y - start_y
23        angle = math.atan2(y_diff, x_diff)
24
25        # What is the 1/2 size of this sprite, so we can figure out how far
26        # away to spawn the bullet
27        size = max(self.player_sprite.width, self.player_sprite.height) / 2
28
29        # Use angle to to spawn bullet away from player in proper direction
30        bullet.center_x += size * math.cos(angle)
31        bullet.center_y += size * math.sin(angle)
32
33        # Set angle of bullet
34        bullet.angle = math.degrees(angle)
35
36        # Gravity to use for the bullet
37        # If we don't use custom gravity, bullet drops too fast, or we have
38        # to make it go too fast.
39        # Force is in relation to bullet's angle.
40        bullet_gravity = (0, -BULLET_GRAVITY)
41
42        # Add the sprite. This needs to be done AFTER setting the fields above.
43        self.physics_engine.add_sprite(bullet,
44                                       mass=BULLET_MASS,
45                                       damping=1.0,
46                                       friction=0.6,
47                                       collision_type="bullet",
48                                       gravity=bullet_gravity,
49                                       elasticity=0.9)
50
51        # Add force to bullet
52        force = (BULLET_MOVE_FORCE, 0)
53        self.physics_engine.apply_force(bullet, force)

Destroy Bullets and Items

This section has two goals:

  • Get rid of the bullet if it flies off-screen

  • Handle collisions of the bullet and other items

Destroy Bullet If It Goes Off-Screen

First, we’ll create a custom bullet class. This class will define the pymunk_moved method, and check our location each time the bullet moves. If our y value is too low, we’ll remove the bullet.

Destroy Bullets - Bullet Sprite
1class BulletSprite(arcade.SpriteSolidColor):
2    """ Bullet Sprite """
3    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
4        """ Handle when the sprite is moved by the physics engine. """
5        # If the bullet falls below the screen, remove it
6        if self.center_y < -100:
7            self.remove_from_sprite_lists()

And, of course, once we create the bullet we have to update our code to use it instead of the plain arcade.Sprite class.

Destroy Bullets - Bullet Sprite
1    def on_mouse_press(self, x, y, button, modifiers):
2        """ Called whenever the mouse button is clicked. """
3
4        bullet = BulletSprite(20, 5, arcade.color.DARK_YELLOW)
5        self.bullet_list.append(bullet)
Handle Collisions

To handle collisions, we can add custom collision handler call-backs. If you’ll remember when we added items to the physics engine, we gave each item a collision type, such as “wall” or “bullet” or “item”. We can write a function and register it to handle all bullet/wall collisions.

In this case, bullets that hit a wall go away. Bullets that hit items cause both the item and the bullet to go away. We could also add code to track damage to a sprite, only removing it after so much damage was applied. Even changing the texture depending on its health.

Destroy Bullets - Collision Handlers
 1        def wall_hit_handler(bullet_sprite, _wall_sprite, _arbiter, _space, _data):
 2            """ Called for bullet/wall collision """
 3            bullet_sprite.remove_from_sprite_lists()
 4
 5        self.physics_engine.add_collision_handler("bullet", "wall", post_handler=wall_hit_handler)
 6
 7        def item_hit_handler(bullet_sprite, item_sprite, _arbiter, _space, _data):
 8            """ Called for bullet/wall collision """
 9            bullet_sprite.remove_from_sprite_lists()
10            item_sprite.remove_from_sprite_lists()
11
12        self.physics_engine.add_collision_handler("bullet", "item", post_handler=item_hit_handler)

Add Moving Platforms

We can add support for moving platforms. Platforms can be added in an object layer. An object layer allows platforms to be placed anywhere, and not just on exact grid locations. Object layers also allow us to add custom properties for each tile we place.

_images/add_object_layer.png

Adding an object layer.

Once we have the tile placed, we can add custom properties for it. Click the ‘+’ icon and add properties for all or some of:

  • change_x

  • change_y

  • left_boundary

  • right_boundary

  • top_boundary

  • bottom_boundary

If these are named exact matches, they’ll automatically copy their values into the sprite attributes of the same name.

_images/add_custom_properties.png

Adding custom properties.

Now we need to update our code. In GameWindow.__init__ add a line to create an attribute for moving_sprites_list:

Moving Platforms - Adding the sprite list
        self.moving_sprites_list: Optional[arcade.SpriteList] = None

In the setup method, load in the sprite list from the tmx layer.

Moving Platforms - Adding the sprite list
        self.moving_sprites_list = arcade.tilemap.process_layer(my_map,
                                                                'Moving Platforms',
                                                                SPRITE_SCALING_TILES)

Also in the setup method, we need to add these sprites to the physics engine. In this case we’ll add the sprites as KINEMATIC. Static sprites don’t move. Dynamic sprites move, and can have forces applied to them by other objects. Kinematic sprites do move, but aren’t affected by other objects.

Moving Platforms - Loading the sprites
        # Add kinematic sprites
        self.physics_engine.add_sprite_list(self.moving_sprites_list,
                                            body_type=arcade.PymunkPhysicsEngine.KINEMATIC)

We need to draw the moving platform sprites. After adding this line, you should be able to run the program and see the sprites from this layer, even if they don’t move yet.

Moving Platforms - Draw the sprites
1    def on_draw(self):
2        """ Draw everything """
3        arcade.start_render()
4        self.wall_list.draw()
5        self.moving_sprites_list.draw()
6        self.bullet_list.draw()
7        self.item_list.draw()
8        self.player_list.draw()

Next up, we need to get the sprites moving. First, we’ll check to see if there are any boundaries set, and if we need to reverse our direction.

After that we’ll create a velocity vector. Velocity is in pixels per second. In this case, I’m assuming the user set the velocity in pixels per frame in Tiled instead, so we’ll convert.

Warning

Changing center_x and center_y will not move the sprite. If you want to change a sprite’s position, use the physics engine’s set_position method.

Also, setting an item’s position “teleports” it there. The physics engine will happily move the object right into another object. Setting the item’s velocity instead will cause the physics engine to move the item, pushing any dynamic items out of the way.

Moving Platforms - Moving the sprites
        # For each moving sprite, see if we've reached a boundary and need to
        # reverse course.
        for moving_sprite in self.moving_sprites_list:
            if moving_sprite.boundary_right and \
                    moving_sprite.change_x > 0 and \
                    moving_sprite.right > moving_sprite.boundary_right:
                moving_sprite.change_x *= -1
            elif moving_sprite.boundary_left and \
                    moving_sprite.change_x < 0 and \
                    moving_sprite.left > moving_sprite.boundary_left:
                moving_sprite.change_x *= -1
            if moving_sprite.boundary_top and \
                    moving_sprite.change_y > 0 and \
                    moving_sprite.top > moving_sprite.boundary_top:
                moving_sprite.change_y *= -1
            elif moving_sprite.boundary_bottom and \
                    moving_sprite.change_y < 0 and \
                    moving_sprite.bottom < moving_sprite.boundary_bottom:
                moving_sprite.change_y *= -1

            # Figure out and set our moving platform velocity.
            # Pymunk uses velocity is in pixels per second. If we instead have
            # pixels per frame, we need to convert.
            velocity = (moving_sprite.change_x * 1 / delta_time, moving_sprite.change_y * 1 / delta_time)

Add Ladders

The first step to adding ladders to our platformer is modify the __init__ to track some more items:

  • Have a reference to a list of ladder sprites

  • Add textures for a climbing animation

  • Keep track of our movement in the y direction

  • Add a boolean to track if we are on/off a ladder

Add Ladders - PlayerSprite class
 1    def __init__(self,
 2                 ladder_list: arcade.SpriteList,
 3                 hit_box_algorithm):
 4        """ Init """
 5        # Let parent initialize
 6        super().__init__()
 7
 8        # Set our scale
 9        self.scale = SPRITE_SCALING_PLAYER
10
11        # Images from Kenney.nl's Character pack
12        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
13        main_path = ":resources:images/animated_characters/female_person/femalePerson"
14        # main_path = ":resources:images/animated_characters/male_person/malePerson"
15        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
16        # main_path = ":resources:images/animated_characters/zombie/zombie"
17        # main_path = ":resources:images/animated_characters/robot/robot"
18
19        # Load textures for idle standing
20        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png",
21                                                          hit_box_algorithm=hit_box_algorithm)
22        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")
23        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")
24
25        # Load textures for walking
26        self.walk_textures = []
27        for i in range(8):
28            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
29            self.walk_textures.append(texture)
30
31        # Load textures for climbing
32        self.climbing_textures = []
33        texture = arcade.load_texture(f"{main_path}_climb0.png")
34        self.climbing_textures.append(texture)
35        texture = arcade.load_texture(f"{main_path}_climb1.png")
36        self.climbing_textures.append(texture)
37
38        # Set the initial texture
39        self.texture = self.idle_texture_pair[0]
40
41        # Hit box will be set based on the first image used.
42        self.hit_box = self.texture.hit_box_points
43
44        # Default to face-right
45        self.character_face_direction = RIGHT_FACING
46
47        # Index of our current texture
48        self.cur_texture = 0
49
50        # How far have we traveled horizontally since changing the texture
51        self.x_odometer = 0
52        self.y_odometer = 0
53
54        self.ladder_list = ladder_list
55        self.is_on_ladder = False

Next, in our pymunk_moved method we need to change physics when we are on a ladder, and to update our player texture.

When we are on a ladder, we’ll turn off gravity, turn up damping, and turn down our max vertical velocity. If we are off the ladder, reset those attributes.

When we are on a ladder, but not on the ground, we’ll alternate between a couple climbing textures.

Add Ladders - PlayerSprite class
 1    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
 2        """ Handle being moved by the pymunk engine """
 3        # Figure out if we need to face left or right
 4        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
 5            self.character_face_direction = LEFT_FACING
 6        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
 7            self.character_face_direction = RIGHT_FACING
 8
 9        # Are we on the ground?
10        is_on_ground = physics_engine.is_on_ground(self)
11
12        # Are we on a ladder?
13        if len(arcade.check_for_collision_with_list(self, self.ladder_list)) > 0:
14            if not self.is_on_ladder:
15                self.is_on_ladder = True
16                self.pymunk.gravity = (0, 0)
17                self.pymunk.damping = 0.0001
18                self.pymunk.max_vertical_velocity = PLAYER_MAX_HORIZONTAL_SPEED
19        else:
20            if self.is_on_ladder:
21                self.pymunk.damping = 1.0
22                self.pymunk.max_vertical_velocity = PLAYER_MAX_VERTICAL_SPEED
23                self.is_on_ladder = False
24                self.pymunk.gravity = None
25
26        # Add to the odometer how far we've moved
27        self.x_odometer += dx
28        self.y_odometer += dy
29
30        if self.is_on_ladder and not is_on_ground:
31            # Have we moved far enough to change the texture?
32            if abs(self.y_odometer) > DISTANCE_TO_CHANGE_TEXTURE:
33
34                # Reset the odometer
35                self.y_odometer = 0
36
37                # Advance the walking animation
38                self.cur_texture += 1
39
40            if self.cur_texture > 1:
41                self.cur_texture = 0
42            self.texture = self.climbing_textures[self.cur_texture]
43            return
44
45        # Jumping animation
46        if not is_on_ground:
47            if dy > DEAD_ZONE:
48                self.texture = self.jump_texture_pair[self.character_face_direction]
49                return
50            elif dy < -DEAD_ZONE:
51                self.texture = self.fall_texture_pair[self.character_face_direction]
52                return
53
54        # Idle animation
55        if abs(dx) <= DEAD_ZONE:
56            self.texture = self.idle_texture_pair[self.character_face_direction]
57            return
58
59        # Have we moved far enough to change the texture?
60        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:
61
62            # Reset the odometer
63            self.x_odometer = 0
64
65            # Advance the walking animation
66            self.cur_texture += 1
67            if self.cur_texture > 7:
68                self.cur_texture = 0
69            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]

Then we just need to add a few variables to the __init__ to track ladders:

Add Ladders - Game Window Init
 1    def __init__(self, width, height, title):
 2        """ Create the variables """
 3
 4        # Init the parent class
 5        super().__init__(width, height, title)
 6
 7        # Player sprite
 8        self.player_sprite: Optional[PlayerSprite] = None
 9
10        # Sprite lists we need
11        self.player_list: Optional[arcade.SpriteList] = None
12        self.wall_list: Optional[arcade.SpriteList] = None
13        self.bullet_list: Optional[arcade.SpriteList] = None
14        self.item_list: Optional[arcade.SpriteList] = None
15        self.moving_sprites_list: Optional[arcade.SpriteList] = None
16        self.ladder_list: Optional[arcade.SpriteList] = None
17
18        # Track the current state of what key is pressed
19        self.left_pressed: bool = False
20        self.right_pressed: bool = False
21        self.up_pressed: bool = False
22        self.down_pressed: bool = False
23
24        # Physics engine
25        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]
26
27        # Set background color
28        arcade.set_background_color(arcade.color.AMAZON)

Then load the ladder layer in setup:

Add Ladders - Game Window Setup
        self.wall_list = arcade.tilemap.process_layer(my_map,
                                                      'Platforms',
                                                      SPRITE_SCALING_TILES,
                                                      hit_box_algorithm="Detailed")

Also, pass the ladder list to the player class:

Add Ladders - Game Window Setup
                                                      'Dynamic Items',
                                                      SPRITE_SCALING_TILES,

Then change the jump button so that we don’t jump if we are on a ladder. Also, we want to track if the up key, or down key are pressed.

Add Ladders - Game Window Key Down
 1    def on_key_press(self, key, modifiers):
 2        """Called whenever a key is pressed. """
 3
 4        if key == arcade.key.LEFT:
 5            self.left_pressed = True
 6        elif key == arcade.key.RIGHT:
 7            self.right_pressed = True
 8        elif key == arcade.key.UP:
 9            self.up_pressed = True
10            # find out if player is standing on ground, and not on a ladder
11            if self.physics_engine.is_on_ground(self.player_sprite) \
12                    and not self.player_sprite.is_on_ladder:
13                # She is! Go ahead and jump
14                impulse = (0, PLAYER_JUMP_IMPULSE)
15                self.physics_engine.apply_impulse(self.player_sprite, impulse)
16        elif key == arcade.key.DOWN:
17            self.down_pressed = True

Add to the key up handler tracking for which key is pressed.

Add Ladders - Game Window Key Up
 1    def on_key_release(self, key, modifiers):
 2        """Called when the user releases a key. """
 3
 4        if key == arcade.key.LEFT:
 5            self.left_pressed = False
 6        elif key == arcade.key.RIGHT:
 7            self.right_pressed = False
 8        elif key == arcade.key.UP:
 9            self.up_pressed = False
10        elif key == arcade.key.DOWN:
11            self.down_pressed = False

Next, change our update with new updates for the ladder.

Add Ladders - Game Window On Update
 1                                       friction=0.6,
 2                                       collision_type="bullet",
 3                                       gravity=bullet_gravity,
 4                                       elasticity=0.9)
 5
 6        # Add force to bullet
 7        force = (BULLET_MOVE_FORCE, 0)
 8        self.physics_engine.apply_force(bullet, force)
 9
10    def on_update(self, delta_time):
11        """ Movement and game logic """
12
13        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
14        # Update player forces based on keys pressed
15        if self.left_pressed and not self.right_pressed:
16            # Create a force to the left. Apply it.
17            if is_on_ground or self.player_sprite.is_on_ladder:
18                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
19            else:
20                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)
21            self.physics_engine.apply_force(self.player_sprite, force)
22            # Set friction to zero for the player while moving
23            self.physics_engine.set_friction(self.player_sprite, 0)
24        elif self.right_pressed and not self.left_pressed:
25            # Create a force to the right. Apply it.
26            if is_on_ground or self.player_sprite.is_on_ladder:
27                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
28            else:
29                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)
30            self.physics_engine.apply_force(self.player_sprite, force)
31            # Set friction to zero for the player while moving
32            self.physics_engine.set_friction(self.player_sprite, 0)
33        elif self.up_pressed and not self.down_pressed:
34            # Create a force to the right. Apply it.
35            if self.player_sprite.is_on_ladder:
36                force = (0, PLAYER_MOVE_FORCE_ON_GROUND)
37                self.physics_engine.apply_force(self.player_sprite, force)

And, of course, don’t forget to draw the ladders:

Add Ladders - Game Window Key Down
1    def on_draw(self):
2        """ Draw everything """
3        arcade.start_render()
4        self.wall_list.draw()
5        self.ladder_list.draw()
6        self.moving_sprites_list.draw()
7        self.bullet_list.draw()
8        self.item_list.draw()
9        self.player_list.draw()

Add “Hit” Ability

(To be done)

Add “Grab” Ability

(To be done)

Add “Claw-Shot” Ability

(To be done)

Using Views for Start/End Screens

_images/screen-switch.svg

Views allow you to easily switch “views” for what you are showing on the window. You can use this to support adding screens such as:

  • Start screens

  • Instruction screens

  • Game over screens

  • Pause screens

The View class is a lot like the Window class that you are already used to. The View class has methods for on_update and on_draw just like Window. We can change the current view to quickly change the code that is managing what is drawn on the window and handling user input.

If you know ahead of time you want to use views, you can build your code around the Instruction Screens and Game Over Screens. However, typically a programmer wants to add these items to a game that already exists.

This tutorial steps you through how to do just that.

Change Main Program to Use a View

_images/collect-coins-game.png

First, we’ll start with a simple collect coins example: 01_views.py Full Listing

Then we’ll move our game into a game view. Take the code where we define our window class:

class MyGame(arcade.Window):

Change it to derive from arcade.View instead of arcade.Window. I also suggest using “View” as part of the name:

class GameView(arcade.View):

This will require a couple other updates. The View class does not control the size of the window, so we’ll need to take that out of the call to the parent class. Change:

super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)

to:

super().__init__()

The Window class still controls if the mouse is visible or not, so to hide the mouse, we’ll need to use the window attribute that is part of the View class. Change:

self.set_mouse_visible(False)

to:

self.window.set_mouse_visible(False)

Now in the main function, instead of just creating a window, we’ll create a window, a view, and then show that view.

Add views - Main method
1def main():
2    """ Main method """
3
4    window = arcade.Window(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
5    start_view = GameView()
6    window.show_view(start_view)
7    start_view.setup()
8    arcade.run()

At this point, run your game and make sure that it still operates properly. It should run just like it did before, but now we are set up to add additional views.

Add Instruction Screen

_images/instruction_screen.png

Now we are ready to add in our instruction screen as a view. Create a class for it:

class InstructionView(arcade.View):

Then we need to define the on_show method that will be run once when we switch to this view. In this case, we don’t need to do much, just set the background color. If the game is one that scrolls, we’ll also need to reset the viewport so that (0, 0) is back to the lower-left coordinate.

Add views - on_show
    def on_show(self):
        """ This is run once when we switch to this view """
        arcade.set_background_color(arcade.csscolor.DARK_SLATE_BLUE)

        # Reset the viewport, necessary if we have a scrolling game and we need
        # to reset the viewport back to the start so we can see what we draw.
        arcade.set_viewport(0, SCREEN_WIDTH - 1, 0, SCREEN_HEIGHT - 1)

The on_draw method works just like the window class’s method, but it will only be called when this view is active.

In this case, we’ll just draw some text for the instruction screen. Another alternative is to make a graphic in a paint program, and show that image. We’ll do that below where we show the Game Over screen.

Add views - on_draw
    def on_draw(self):
        """ Draw this view """
        arcade.start_render()
        arcade.draw_text("Instructions Screen", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2,
                         arcade.color.WHITE, font_size=50, anchor_x="center")
        arcade.draw_text("Click to advance", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2-75,
                         arcade.color.WHITE, font_size=20, anchor_x="center")

Then we’ll put in a method to respond to a mouse click. Here we’ll create our GameView and call the setup method.

Add views - on_mouse_press
    def on_mouse_press(self, _x, _y, _button, _modifiers):
        """ If the user presses the mouse button, start the game. """
        game_view = GameView()
        game_view.setup()
        self.window.show_view(game_view)

Now we need to go back to the main method. Instead of creating a GameView it needs to now create an InstructionView.

Add views - Main method
1def main():
2    """ Main method """
3
4    window = arcade.Window(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
5    start_view = InstructionView()
6    window.show_view(start_view)
7    arcade.run()

Game Over Screen

_images/game_over_screenshot.png

Another way of doing instruction, pause, and game over screens is with a graphic. In this example, we’ve created a separate image with the same size as our window (800x600) and saved it as game_over.png. You can use the Windows “Paint” app or get an app for your Mac to make images in order to do this yourself.

The new GameOverView view that we are adding loads in the game over screen image as a texture in its __init__. The on_draw method draws that texture to the screen. By using an image, we can fancy up the game over screen using an image editor as much as we want, while keeping the code simple.

When the user clicks the mouse button, we just start the game over.

Add views - Game Over View
 1class GameOverView(arcade.View):
 2    """ View to show when game is over """
 3
 4    def __init__(self):
 5        """ This is run once when we switch to this view """
 6        super().__init__()
 7        self.texture = arcade.load_texture("game_over.png")
 8
 9        # Reset the viewport, necessary if we have a scrolling game and we need
10        # to reset the viewport back to the start so we can see what we draw.
11        arcade.set_viewport(0, SCREEN_WIDTH - 1, 0, SCREEN_HEIGHT - 1)
12
13    def on_draw(self):
14        """ Draw this view """
15        arcade.start_render()
16        self.texture.draw_sized(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2,
17                                SCREEN_WIDTH, SCREEN_HEIGHT)
18
19    def on_mouse_press(self, _x, _y, _button, _modifiers):
20        """ If the user presses the mouse button, re-start the game. """
21        game_view = GameView()
22        game_view.setup()
23        self.window.show_view(game_view)

The last thing we need, is to trigger the “Game Over” view. In our GameView.on_update method, we can check the list length. As soon as it hits zero, we’ll change our view.

Add views - Game Over View
 1    def on_update(self, delta_time):
 2        """ Movement and game logic """
 3
 4        # Call update on all sprites (The sprites don't do much in this
 5        # example though.)
 6        self.coin_list.update()
 7
 8        # Generate a list of all sprites that collided with the player.
 9        coins_hit_list = arcade.check_for_collision_with_list(self.player_sprite, self.coin_list)
10
11        # Loop through each colliding sprite, remove it, and add to the score.
12        for coin in coins_hit_list:
13            coin.remove_from_sprite_lists()
14            self.score += 1
15
16        # Check length of coin list. If it is zero, flip to the
17        # game over view.
18        if len(self.coin_list) == 0:
19            view = GameOverView()
20            self.window.show_view(view)

Solitaire

_images/animated.gif

This solitaire tutorial takes you though the basics of creating a card game, and doing extensive drag/drop work.

Open a Window

_images/solitaire_01.png

To begin with, let’s start with a program that will use Arcade to open a blank window. The listing below also has stubs for methods we’ll fill in later.

Get started with this code and make sure you can run it. It should pop open a green window.

Starting Program
 1"""
 2Solitaire clone.
 3"""
 4import arcade
 5
 6# Screen title and size
 7SCREEN_WIDTH = 1024
 8SCREEN_HEIGHT = 768
 9SCREEN_TITLE = "Drag and Drop Cards"
10
11class MyGame(arcade.Window):
12    """ Main application class. """
13
14    def __init__(self):
15        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
16
17        arcade.set_background_color(arcade.color.AMAZON)
18
19    def setup(self):
20        """ Set up the game here. Call this function to restart the game. """
21        pass
22
23    def on_draw(self):
24        """ Render the screen. """
25        # Clear the screen
26        arcade.start_render()
27
28    def on_mouse_press(self, x, y, button, key_modifiers):
29        """ Called when the user presses a mouse button. """
30        pass
31
32    def on_mouse_release(self, x: float, y: float, button: int,
33                         modifiers: int):
34        """ Called when the user presses a mouse button. """
35        pass
36
37    def on_mouse_motion(self, x: float, y: float, dx: float, dy: float):
38        """ User moves mouse """
39        pass
40
41
42def main():
43    """ Main method """
44    window = MyGame()
45    window.setup()
46    arcade.run()
47
48
49if __name__ == "__main__":
50    main()

Create Card Sprites

Our next step is the create a bunch of sprites, one for each card.

Constants

First, we’ll create some constants used in positioning the cards, and keeping track of what card is which.

We could just hard-code numbers, but I like to calculate things out. The “mat” will eventually be a square slightly larger than each card that tracks where we can put cards. (A mat where we can put a pile of cards on.)

Create constants for positioning
 1# Constants for sizing
 2CARD_SCALE = 0.6
 3
 4# How big are the cards?
 5CARD_WIDTH = 140 * CARD_SCALE
 6CARD_HEIGHT = 190 * CARD_SCALE
 7
 8# How big is the mat we'll place the card on?
 9MAT_PERCENT_OVERSIZE = 1.25
10MAT_HEIGHT = int(CARD_HEIGHT * MAT_PERCENT_OVERSIZE)
11MAT_WIDTH = int(CARD_WIDTH * MAT_PERCENT_OVERSIZE)
12
13# How much space do we leave as a gap between the mats?
14# Done as a percent of the mat size.
15VERTICAL_MARGIN_PERCENT = 0.10
16HORIZONTAL_MARGIN_PERCENT = 0.10
17
18# The Y of the bottom row (2 piles)
19BOTTOM_Y = MAT_HEIGHT / 2 + MAT_HEIGHT * VERTICAL_MARGIN_PERCENT
20
21# The X of where to start putting things on the left side
22START_X = MAT_WIDTH / 2 + MAT_WIDTH * HORIZONTAL_MARGIN_PERCENT
23
24# Card constants
25CARD_VALUES = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
26CARD_SUITS = ["Clubs", "Hearts", "Spades", "Diamonds"]
Card Class

Next up, we’ll create a card class. The card class is a subclass of arcade.Sprite. It will have attributes for the suit and value of the card, and auto-load the image for the card based on that.

We’ll use the entire image as the hit box, so we don’t need to go through the time consuming hit box calculation. Therefore we turn that off. Otherwise loading the sprites would take a long time.

Create card sprites
 1class Card(arcade.Sprite):
 2    """ Card sprite """
 3
 4    def __init__(self, suit, value, scale=1):
 5        """ Card constructor """
 6
 7        # Attributes for suit and value
 8        self.suit = suit
 9        self.value = value
10
11        # Image to use for the sprite when face up
12        self.image_file_name = f":resources:images/cards/card{self.suit}{self.value}.png"
13
14        # Call the parent
15        super().__init__(self.image_file_name, scale, hit_box_algorithm="None")
Creating Cards

We’ll start by creating an attribute for the SpriteList that will hold all the cards in the game.

Create card sprites
1    def __init__(self):
2        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
3
4        # Sprite list with all the cards, no matter what pile they are in.
5        self.card_list = None
6
7        arcade.set_background_color(arcade.color.AMAZON)

In setup we’ll create the list and the cards. We don’t do this in __init__ because by separating the creation into its own method, we can easily restart the game by calling setup.

Create card sprites
 1    def setup(self):
 2        """ Set up the game here. Call this function to restart the game. """
 3
 4        # Sprite list with all the cards, no matter what pile they are in.
 5        self.card_list = arcade.SpriteList()
 6
 7        # Create every card
 8        for card_suit in CARD_SUITS:
 9            for card_value in CARD_VALUES:
10                card = Card(card_suit, card_value, CARD_SCALE)
11                card.position = START_X, BOTTOM_Y
12                self.card_list.append(card)
Drawing Cards

Finally, draw the cards:

Create card sprites
1    def on_draw(self):
2        """ Render the screen. """
3        # Clear the screen
4        arcade.start_render()
5
6        # Draw the cards
7        self.card_list.draw()

You should end up with all the cards stacked in the lower-left corner:

_images/solitaire_02.png

Implement Drag and Drop

Next up, let’s add the ability to pick up, drag, and drop the cards.

Track the Cards

First, let’s add attributes to track what cards we are moving. Because we can move multiple cards, we’ll keep this as a list. If the user drops the card in an illegal spot, we’ll need to reset the card to its original position. So we’ll also track that.

Create the attributes:

Add attributes to __init__
 1    def __init__(self):
 2        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 3
 4        # Sprite list with all the cards, no matter what pile they are in.
 5        self.card_list = None
 6
 7        arcade.set_background_color(arcade.color.AMAZON)
 8
 9        # List of cards we are dragging with the mouse
10        self.held_cards = None
11
12        # Original location of cards we are dragging with the mouse in case
13        # they have to go back.
14        self.held_cards_original_position = None

Set the initial values (an empty list):

Create empty list attributes
 1    def setup(self):
 2        """ Set up the game here. Call this function to restart the game. """
 3
 4        # List of cards we are dragging with the mouse
 5        self.held_cards = []
 6
 7        # Original location of cards we are dragging with the mouse in case
 8        # they have to go back.
 9        self.held_cards_original_position = []
10
11        # Sprite list with all the cards, no matter what pile they are in.
12        self.card_list = arcade.SpriteList()
13
14        # Create every card
15        for card_suit in CARD_SUITS:
16            for card_value in CARD_VALUES:
17                card = Card(card_suit, card_value, CARD_SCALE)
18                card.position = START_X, BOTTOM_Y
19                self.card_list.append(card)
Pull Card to Top of Draw Order

When we click on the card, we’ll want it to be the last card drawn, so it appears on top of all the other cards. Otherwise we might drag a card underneath another card, which would look odd.

Pull card to top
1    def pull_to_top(self, card):
2        """ Pull card to top of rendering order (last to render, looks on-top) """
3        # Find the index of the card
4        index = self.card_list.index(card)
5        # Loop and pull all the other cards down towards the zero end
6        for i in range(index, len(self.card_list) - 1):
7            self.card_list[i] = self.card_list[i + 1]
8        # Put this card at the right-side/top/size of list
9        self.card_list[len(self.card_list) - 1] = card
Mouse Button Pressed

When the user presses the mouse button, we will:

  • See if they clicked on a card

  • If so, put that card in our held cards list

  • Save the original position of the card

  • Pull it to the top of the draw order

Pull card to top
 1    def on_mouse_press(self, x, y, button, key_modifiers):
 2        """ Called when the user presses a mouse button. """
 3
 4        # Get list of cards we've clicked on
 5        cards = arcade.get_sprites_at_point((x, y), self.card_list)
 6
 7        # Have we clicked on a card?
 8        if len(cards) > 0:
 9
10            # Might be a stack of cards, get the top one
11            primary_card = cards[-1]
12
13            # All other cases, grab the face-up card we are clicking on
14            self.held_cards = [primary_card]
15            # Save the position
16            self.held_cards_original_position = [self.held_cards[0].position]
17            # Put on top in drawing order
18            self.pull_to_top(self.held_cards[0])
Mouse Moved

If the user moves the mouse, we’ll move any held cards with it.

Pull card to top
1    def on_mouse_motion(self, x: float, y: float, dx: float, dy: float):
2        """ User moves mouse """
3
4        # If we are holding cards, move them with the mouse
5        for card in self.held_cards:
6            card.center_x += dx
7            card.center_y += dy
Mouse Released

When the user releases the mouse button, we’ll clear the held card list.

Pull card to top
 1    def on_mouse_release(self, x: float, y: float, button: int,
 2                         modifiers: int):
 3        """ Called when the user presses a mouse button. """
 4
 5        # If we don't have any cards, who cares
 6        if len(self.held_cards) == 0:
 7            return
 8
 9        # We are no longer holding cards
10        self.held_cards = []
Test the Program

You should now be able to pick up and move cards around the screen. Try it out!

_images/solitaire_03.png

Draw Pile Mats

Next, we’ll create sprites that will act as guides to where the piles of cards go in our game. We’ll create these as sprites, so we can use collision detection to figure out of we are dropping a card on them or not.

Create Constants

First, we’ll create constants for the middle row of seven piles, and for the top row of four piles. We’ll also create a constant for how far apart each pile should be.

Again, we could hard-code numbers, but I like calculating them so I can change the scale easily.

Add constants
1# The Y of the top row (4 piles)
2TOP_Y = SCREEN_HEIGHT - MAT_HEIGHT / 2 - MAT_HEIGHT * VERTICAL_MARGIN_PERCENT
3
4# The Y of the middle row (7 piles)
5MIDDLE_Y = TOP_Y - MAT_HEIGHT - MAT_HEIGHT * VERTICAL_MARGIN_PERCENT
6
7# How far apart each pile goes
8X_SPACING = MAT_WIDTH + MAT_WIDTH * HORIZONTAL_MARGIN_PERCENT
Create Mat Sprites

Create an attribute for the mat sprite list:

Create the mat sprites
 1    def __init__(self):
 2        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
 3
 4        # Sprite list with all the cards, no matter what pile they are in.
 5        self.card_list = None
 6
 7        arcade.set_background_color(arcade.color.AMAZON)
 8
 9        # List of cards we are dragging with the mouse
10        self.held_cards = None
11
12        # Original location of cards we are dragging with the mouse in case
13        # they have to go back.
14        self.held_cards_original_position = None
15
16        # Sprite list with all the mats tha cards lay on.
17        self.pile_mat_list = None

Then create the mat sprites in the setup method

Create the mat sprites
 1    def setup(self):
 2        """ Set up the game here. Call this function to restart the game. """
 3
 4        # List of cards we are dragging with the mouse
 5        self.held_cards = []
 6
 7        # Original location of cards we are dragging with the mouse in case
 8        # they have to go back.
 9        self.held_cards_original_position = []
10
11        # ---  Create the mats the cards go on.
12
13        # Sprite list with all the mats tha cards lay on.
14        self.pile_mat_list: arcade.SpriteList = arcade.SpriteList()
15
16        # Create the mats for the bottom face down and face up piles
17        pile = arcade.SpriteSolidColor(MAT_WIDTH, MAT_HEIGHT, arcade.csscolor.DARK_OLIVE_GREEN)
18        pile.position = START_X, BOTTOM_Y
19        self.pile_mat_list.append(pile)
20
21        pile = arcade.SpriteSolidColor(MAT_WIDTH, MAT_HEIGHT, arcade.csscolor.DARK_OLIVE_GREEN)
22        pile.position = START_X + X_SPACING, BOTTOM_Y
23        self.pile_mat_list.append(pile)
24
25        # Create the seven middle piles
26        for i in range(7):
27            pile = arcade.SpriteSolidColor(MAT_WIDTH, MAT_HEIGHT, arcade.csscolor.DARK_OLIVE_GREEN)
28            pile.position = START_X + i * X_SPACING, MIDDLE_Y
29            self.pile_mat_list.append(pile)
30
31        # Create the top "play" piles
32        for i in range(4):
33            pile = arcade.SpriteSolidColor(MAT_WIDTH, MAT_HEIGHT, arcade.csscolor.DARK_OLIVE_GREEN)
34            pile.position = START_X + i * X_SPACING, TOP_Y
35            self.pile_mat_list.append(pile)
36
37        # Sprite list with all the cards, no matter what pile they are in.
38        self.card_list = arcade.SpriteList()
39
40        # Create every card
41        for card_suit in CARD_SUITS:
42            for card_value in CARD_VALUES:
43                card = Card(card_suit, card_value, CARD_SCALE)
44                card.position = START_X, BOTTOM_Y
45                self.card_list.append(card)
Draw Mat Sprites

Finally, the mats aren’t going to display if we don’t draw them:

Draw the mat sprites
 1    def on_draw(self):
 2        """ Render the screen. """
 3        # Clear the screen
 4        arcade.start_render()
 5
 6        # Draw the mats the cards go on to
 7        self.pile_mat_list.draw()
 8
 9        # Draw the cards
10        self.card_list.draw()
Test the Program

Run the program, and see if the mats appear:

_images/solitaire_04.png

Snap Cards to Piles

Right now, you can drag the cards anywhere. They don’t have to go onto a pile. Let’s add code that “snaps” the card onto a pile. If we don’t drop on a pile, let’s reset back to the original location.

Snap to nearest pile
 1    def on_mouse_release(self, x: float, y: float, button: int,
 2                         modifiers: int):
 3        """ Called when the user presses a mouse button. """
 4
 5        # If we don't have any cards, who cares
 6        if len(self.held_cards) == 0:
 7            return
 8
 9        # Find the closest pile, in case we are in contact with more than one
10        pile, distance = arcade.get_closest_sprite(self.held_cards[0], self.pile_mat_list)
11        reset_position = True
12
13        # See if we are in contact with the closest pile
14        if arcade.check_for_collision(self.held_cards[0], pile):
15
16            # For each held card, move it to the pile we dropped on
17            for i, dropped_card in enumerate(self.held_cards):
18                # Move cards to proper position
19                dropped_card.position = pile.center_x, pile.center_y
20
21            # Success, don't reset position of cards
22            reset_position = False
23
24            # Release on top play pile? And only one card held?
25        if reset_position:
26            # Where-ever we were dropped, it wasn't valid. Reset the each card's position
27            # to its original spot.
28            for pile_index, card in enumerate(self.held_cards):
29                card.position = self.held_cards_original_position[pile_index]
30
31        # We are no longer holding cards
32        self.held_cards = []

Shuffle the Cards

Having all the cards in order is boring. Let’s shuffle them in the setup method:

Shuffle Cards
1        # Shuffle the cards
2        for pos1 in range(len(self.card_list)):
3            pos2 = random.randrange(len(self.card_list))
4            self.card_list[pos1], self.card_list[pos2] = self.card_list[pos2], self.card_list[pos1]

Don’t forget to import random at the top.

Run your program and make sure you can move cards around.

_images/solitaire_06.png

Track Card Piles

Right now we are moving the cards around. But it isn’t easy to figure out what card is in which pile. We could check by position, but then we start fanning the cards out, that will be very difficult.

Therefore we will keep a separate list for each pile of cards. When we move a card we need to move the position, and switch which list it is in.

Add New Constants

To start with, let’s add some constants for each pile:

New Constants
 1# If we fan out cards stacked on each other, how far apart to fan them?
 2CARD_VERTICAL_OFFSET = CARD_HEIGHT * CARD_SCALE * 0.3
 3
 4# Constants that represent "what pile is what" for the game
 5PILE_COUNT = 13
 6BOTTOM_FACE_DOWN_PILE = 0
 7BOTTOM_FACE_UP_PILE = 1
 8PLAY_PILE_1 = 2
 9PLAY_PILE_2 = 3
10PLAY_PILE_3 = 4
11PLAY_PILE_4 = 5
12PLAY_PILE_5 = 6
13PLAY_PILE_6 = 7
14PLAY_PILE_7 = 8
15TOP_PILE_1 = 9
16TOP_PILE_2 = 10
17TOP_PILE_3 = 11
18TOP_PILE_4 = 12
Create the Pile Lists

Then in our __init__ add a variable to track the piles:

Init Method Additions
1        # Create a list of lists, each holds a pile of cards.
2        self.piles = None

In the setup method, create a list for each pile. Then, add all the cards to the face-down deal pile. (Later, we’ll add support for face-down cards. Yes, right now all the cards in the face down pile are up.)

Setup Method Additions
1        # Create a list of lists, each holds a pile of cards.
2        self.piles = [[] for _ in range(PILE_COUNT)]
3
4        # Put all the cards in the bottom face-down pile
5        for card in self.card_list:
6            self.piles[BOTTOM_FACE_DOWN_PILE].append(card)
Card Pile Management Methods

Next, we need some convenience methods we’ll use elsewhere.

First, given a card, return the index of which pile that card belongs to:

get_pile_for_card method
1    def get_pile_for_card(self, card):
2        """ What pile is this card in? """
3        for index, pile in enumerate(self.piles):
4            if card in pile:
5                return index

Next, remove a card from whatever pile it happens to be in.

remove_card_from_pile method
1    def remove_card_from_pile(self, card):
2        """ Remove card from whatever pile it was in. """
3        for pile in self.piles:
4            if card in pile:
5                pile.remove(card)
6                break

Finally, move a card from one pile to another.

move_card_to_new_pile method
1    def move_card_to_new_pile(self, card, pile_index):
2        """ Move the card to a new pile """
3        self.remove_card_from_pile(card)
4        self.piles[pile_index].append(card)
Dropping the Card

Next, we need to modify what happens when we release the mouse.

First, see if we release it onto the same pile it came from. If so, just reset the card back to its original location.

on_mouse_release method
 1    def on_mouse_release(self, x: float, y: float, button: int,
 2                         modifiers: int):
 3        """ Called when the user presses a mouse button. """
 4
 5        # If we don't have any cards, who cares
 6        if len(self.held_cards) == 0:
 7            return
 8
 9        # Find the closest pile, in case we are in contact with more than one
10        pile, distance = arcade.get_closest_sprite(self.held_cards[0], self.pile_mat_list)
11        reset_position = True
12
13        # See if we are in contact with the closest pile
14        if arcade.check_for_collision(self.held_cards[0], pile):
15
16            # What pile is it?
17            pile_index = self.pile_mat_list.index(pile)
18
19            #  Is it the same pile we came from?
20            if pile_index == self.get_pile_for_card(self.held_cards[0]):
21                # If so, who cares. We'll just reset our position.
22                pass

What if it is on a middle play pile? Ugh, that’s a bit complicated. If the mat is empty, we need to place it in the middle of the mat. If there are cards on the mat, we need to offset the card so we can see a spread of cards.

While we can only pick up one card at a time right now, we need to support dropping multiple cards for once we support multiple card carries.

on_mouse_release method
 1            # Is it on a middle play pile?
 2            elif PLAY_PILE_1 <= pile_index <= PLAY_PILE_7:
 3                # Are there already cards there?
 4                if len(self.piles[pile_index]) > 0:
 5                    # Move cards to proper position
 6                    top_card = self.piles[pile_index][-1]
 7                    for i, dropped_card in enumerate(self.held_cards):
 8                        dropped_card.position = top_card.center_x, \
 9                                                top_card.center_y - CARD_VERTICAL_OFFSET * (i + 1)
10                else:
11                    # Are there no cards in the middle play pile?
12                    for i, dropped_card in enumerate(self.held_cards):
13                        # Move cards to proper position
14                        dropped_card.position = pile.center_x, \
15                                                pile.center_y - CARD_VERTICAL_OFFSET * i
16
17                for card in self.held_cards:
18                    # Cards are in the right position, but we need to move them to the right list
19                    self.move_card_to_new_pile(card, pile_index)
20
21                # Success, don't reset position of cards
22                reset_position = False

What if it is released on a top play pile? Make sure that we only have one card we are holding. We don’t want to drop a stack up top. Then move the card to that pile.

on_mouse_release method
1            # Release on top play pile? And only one card held?
2            elif TOP_PILE_1 <= pile_index <= TOP_PILE_4 and len(self.held_cards) == 1:
3                # Move position of card to pile
4                self.held_cards[0].position = pile.position
5                # Move card to card list
6                for card in self.held_cards:
7                    self.move_card_to_new_pile(card, pile_index)
8
9                reset_position = False

If the move is invalid, we need to reset all held cards to their initial location.

on_mouse_release method
1        if reset_position:
2            # Where-ever we were dropped, it wasn't valid. Reset the each card's position
3            # to its original spot.
4            for pile_index, card in enumerate(self.held_cards):
5                card.position = self.held_cards_original_position[pile_index]
6
7        # We are no longer holding cards
8        self.held_cards = []
Test

Test out your program, and see if the cards are being fanned out properly.

Note

The code isn’t enforcing any game rules. You can stack cards in any order. Also, with long stacks of cards, you still have to drop the card on the mat. This is counter-intuitive when the stack of cards extends downwards past the mat.

We leave the solutions to these issues as an exercise for the reader.

_images/solitaire_07.png

Pick Up Card Stacks

How do we pick up a whole stack of cards? When the mouse is pressed, we need to figure out what pile the card is in.

Next, look at where in the pile the card is that we clicked on. If there are any cards later on on the pile, we want to pick up those cards too. Add them to the list.

on_mouse_release method
 1    def on_mouse_press(self, x, y, button, key_modifiers):
 2        """ Called when the user presses a mouse button. """
 3
 4        # Get list of cards we've clicked on
 5        cards = arcade.get_sprites_at_point((x, y), self.card_list)
 6
 7        # Have we clicked on a card?
 8        if len(cards) > 0:
 9
10            # Might be a stack of cards, get the top one
11            primary_card = cards[-1]
12            # Figure out what pile the card is in
13            pile_index = self.get_pile_for_card(primary_card)
14
15            # All other cases, grab the face-up card we are clicking on
16            self.held_cards = [primary_card]
17            # Save the position
18            self.held_cards_original_position = [self.held_cards[0].position]
19            # Put on top in drawing order
20            self.pull_to_top(self.held_cards[0])
21
22            # Is this a stack of cards? If so, grab the other cards too
23            card_index = self.piles[pile_index].index(primary_card)
24            for i in range(card_index + 1, len(self.piles[pile_index])):
25                card = self.piles[pile_index][i]
26                self.held_cards.append(card)
27                self.held_cards_original_position.append(card.position)
28                self.pull_to_top(card)

After this, you should be able to pick up a stack of cards from the middle piles with the mouse and move them around.

Deal Out Cards

We can deal the cards into the seven middle piles by adding some code to the setup method. We need to change the list each card is part of, along with its position.

Setup Method Additions
 1        # - Pull from that pile into the middle piles, all face-down
 2        # Loop for each pile
 3        for pile_no in range(PLAY_PILE_1, PLAY_PILE_7 + 1):
 4            # Deal proper number of cards for that pile
 5            for j in range(pile_no - PLAY_PILE_1 + 1):
 6                # Pop the card off the deck we are dealing from
 7                card = self.piles[BOTTOM_FACE_DOWN_PILE].pop()
 8                # Put in the proper pile
 9                self.piles[pile_no].append(card)
10                # Move card to same position as pile we just put it in
11                card.position = self.pile_mat_list[pile_no].position
12                # Put on top in draw order
13                self.pull_to_top(card)

Face Down Cards

We don’t play solitaire with all the cards facing up, so let’s add face-down support to our game.

New Constants

First define a constant for what image to use when face-down.

Face Down Image Constant
1# Face down image
2FACE_DOWN_IMAGE = ":resources:images/cards/cardBack_red2.png"
Updates to Card Class

Next, default each card in the Card class to be face up. Also, let’s add methods to flip the card up or down.

Updated Card Class
 1class Card(arcade.Sprite):
 2    """ Card sprite """
 3
 4    def __init__(self, suit, value, scale=1):
 5        """ Card constructor """
 6
 7        # Attributes for suit and value
 8        self.suit = suit
 9        self.value = value
10
11        # Image to use for the sprite when face up
12        self.image_file_name = f":resources:images/cards/card{self.suit}{self.value}.png"
13        self.is_face_up = False
14        super().__init__(FACE_DOWN_IMAGE, scale, hit_box_algorithm="None")
15
16    def face_down(self):
17        """ Turn card face-down """
18        self.texture = arcade.load_texture(FACE_DOWN_IMAGE)
19        self.is_face_up = False
20
21    def face_up(self):
22        """ Turn card face-up """
23        self.texture = arcade.load_texture(self.image_file_name)
24        self.is_face_up = True
25
26    @property
27    def is_face_down(self):
28        """ Is this card face down? """
29        return not self.is_face_up
Flip Up Cards On Middle Seven Piles

Right now every card is face down. Let’s update the setup method so the top cards in the middle seven piles are face up.

Flip Up Cards
1        # Flip up the top cards
2        for i in range(PLAY_PILE_1, PLAY_PILE_7 + 1):
3            self.piles[i][-1].face_up()
Flip Up Cards When Clicked

When we click on a card that is face down, instead of picking it up, let’s flip it over:

Flip Up Cards
 1    def on_mouse_press(self, x, y, button, key_modifiers):
 2        """ Called when the user presses a mouse button. """
 3
 4        # Get list of cards we've clicked on
 5        cards = arcade.get_sprites_at_point((x, y), self.card_list)
 6
 7        # Have we clicked on a card?
 8        if len(cards) > 0:
 9
10            # Might be a stack of cards, get the top one
11            primary_card = cards[-1]
12            # Figure out what pile the card is in
13            pile_index = self.get_pile_for_card(primary_card)
14
15            if primary_card.is_face_down:
16                # Is the card face down? In one of those middle 7 piles? Then flip up
17                primary_card.face_up()
18            else:
19                # All other cases, grab the face-up card we are clicking on
20                self.held_cards = [primary_card]
21                # Save the position
22                self.held_cards_original_position = [self.held_cards[0].position]
23                # Put on top in drawing order
24                self.pull_to_top(self.held_cards[0])
25
26                # Is this a stack of cards? If so, grab the other cards too
27                card_index = self.piles[pile_index].index(primary_card)
28                for i in range(card_index + 1, len(self.piles[pile_index])):
29                    card = self.piles[pile_index][i]
30                    self.held_cards.append(card)
31                    self.held_cards_original_position.append(card.position)
32                    self.pull_to_top(card)
Test

Try out your program. As you move cards around, you should see face down cards as well, and be able to flip them over.

_images/solitaire_10.png

Restart Game

We can add the ability to restart are game any type we press the ‘R’ key:

Flip Up Cards
1    def on_key_press(self, symbol: int, modifiers: int):
2        """ User presses key """
3        if symbol == arcade.key.R:
4            # Restart
5            self.setup()

Flip Three From Draw Pile

The draw pile at the bottom of our screen doesn’t work right yet. When we click on it, we need it to flip three cards to the bottom-right pile. Also, if the have gone through all the cards in the pile, we need to reset the pile so we can go through it again.

Flipping of Bottom Deck
 1    def on_mouse_press(self, x, y, button, key_modifiers):
 2        """ Called when the user presses a mouse button. """
 3
 4        # Get list of cards we've clicked on
 5        cards = arcade.get_sprites_at_point((x, y), self.card_list)
 6
 7        # Have we clicked on a card?
 8        if len(cards) > 0:
 9
10            # Might be a stack of cards, get the top one
11            primary_card = cards[-1]
12            # Figure out what pile the card is in
13            pile_index = self.get_pile_for_card(primary_card)
14
15            # Are we clicking on the bottom deck, to flip three cards?
16            if pile_index == BOTTOM_FACE_DOWN_PILE:
17                # Flip three cards
18                for i in range(3):
19                    # If we ran out of cards, stop
20                    if len(self.piles[BOTTOM_FACE_DOWN_PILE]) == 0:
21                        break
22                    # Get top card
23                    card = self.piles[BOTTOM_FACE_DOWN_PILE][-1]
24                    # Flip face up
25                    card.face_up()
26                    # Move card position to bottom-right face up pile
27                    card.position = self.pile_mat_list[BOTTOM_FACE_UP_PILE].position
28                    # Remove card from face down pile
29                    self.piles[BOTTOM_FACE_DOWN_PILE].remove(card)
30                    # Move card to face up list
31                    self.piles[BOTTOM_FACE_UP_PILE].append(card)
32                    # Put on top draw-order wise
33                    self.pull_to_top(card)
34
35            elif primary_card.is_face_down:
36                # Is the card face down? In one of those middle 7 piles? Then flip up
37                primary_card.face_up()
38            else:
39                # All other cases, grab the face-up card we are clicking on
40                self.held_cards = [primary_card]
41                # Save the position
42                self.held_cards_original_position = [self.held_cards[0].position]
43                # Put on top in drawing order
44                self.pull_to_top(self.held_cards[0])
45
46                # Is this a stack of cards? If so, grab the other cards too
47                card_index = self.piles[pile_index].index(primary_card)
48                for i in range(card_index + 1, len(self.piles[pile_index])):
49                    card = self.piles[pile_index][i]
50                    self.held_cards.append(card)
51                    self.held_cards_original_position.append(card.position)
52                    self.pull_to_top(card)
53
54        else:
55
56            # Click on a mat instead of a card?
57            mats = arcade.get_sprites_at_point((x, y), self.pile_mat_list)
58
59            if len(mats) > 0:
60                mat = mats[0]
61                mat_index = self.pile_mat_list.index(mat)
62
63                # Is it our turned over flip mat? and no cards on it?
64                if mat_index == BOTTOM_FACE_DOWN_PILE and len(self.piles[BOTTOM_FACE_DOWN_PILE]) == 0:
65                    # Flip the deck back over so we can restart
66                    temp_list = self.piles[BOTTOM_FACE_UP_PILE].copy()
67                    for card in reversed(temp_list):
68                        card.face_down()
69                        self.piles[BOTTOM_FACE_UP_PILE].remove(card)
70                        self.piles[BOTTOM_FACE_DOWN_PILE].append(card)
71                        card.position = self.pile_mat_list[BOTTOM_FACE_DOWN_PILE].position
Test

Now we’ve got a basic working solitaire game! Try it out!

_images/solitaire_11.png

Conclusion

There’s a lot more that could be added to this game, such as enforcing rules, adding animation to ‘slide’ a dropped card to its position, sound, better graphics, and more. Or this could be adapted to a different card game.

Hopefully this is enough to get you started on your own game.

Lights Tutorial

_images/lights.png

(To be done.)

light_demo.py
  1"""
  2Show how to use lights.
  3
  4.. note:: This uses features from the upcoming version 2.4. The API for these
  5          functions may still change. To use, you will need to install one of the
  6          pre-release packages, or install via GitHub.
  7
  8Artwork from http://kenney.nl
  9
 10"""
 11import arcade
 12from arcade.experimental.lights import Light, LightLayer
 13
 14SCREEN_WIDTH = 1024
 15SCREEN_HEIGHT = 768
 16SCREEN_TITLE = "Lighting Demo"
 17VIEWPORT_MARGIN = 200
 18MOVEMENT_SPEED = 5
 19
 20# This is the color used for 'ambient light'. If you don't want any
 21# ambient light, set it to black.
 22AMBIENT_COLOR = (10, 10, 10)
 23
 24class MyGame(arcade.Window):
 25    """ Main Game Window """
 26
 27    def __init__(self, width, height, title):
 28        """ Set up the class. """
 29        super().__init__(width, height, title, resizable=True)
 30
 31        # Sprite lists
 32        self.background_sprite_list = None
 33        self.player_list = None
 34        self.wall_list = None
 35        self.player_sprite = None
 36
 37        # Physics engine
 38        self.physics_engine = None
 39
 40        # Used for scrolling
 41        self.view_left = 0
 42        self.view_bottom = 0
 43
 44        # --- Light related ---
 45        # List of all the lights
 46        self.light_layer = None
 47        # Individual light we move with player, and turn on/off
 48        self.player_light = None
 49
 50    def setup(self):
 51        """ Create everything """
 52
 53        # Create sprite lists
 54        self.background_sprite_list = arcade.SpriteList()
 55        self.player_list = arcade.SpriteList()
 56        self.wall_list = arcade.SpriteList()
 57
 58        # Create player sprite
 59        self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png", 0.4)
 60        self.player_sprite.center_x = 64
 61        self.player_sprite.center_y = 270
 62        self.player_list.append(self.player_sprite)
 63
 64        # --- Light related ---
 65        # Lights must shine on something. If there is no background sprite or color,
 66        # you will just see black. Therefore, we use a loop to create a whole bunch of brick tiles to go in the
 67        # background.
 68        for x in range(-128, 2000, 128):
 69            for y in range(-128, 1000, 128):
 70                sprite = arcade.Sprite(":resources:images/tiles/brickTextureWhite.png")
 71                sprite.position = x, y
 72                self.background_sprite_list.append(sprite)
 73
 74        # Create a light layer, used to render things to, then post-process and
 75        # add lights. This must match the screen size.
 76        self.light_layer = LightLayer(SCREEN_WIDTH, SCREEN_HEIGHT)
 77        # We can also set the background color that will be lit by lights,
 78        # but in this instance we just want a black background
 79        self.light_layer.set_background_color(arcade.color.BLACK)
 80
 81        # Here we create a bunch of lights.
 82
 83        # Create a small white light
 84        x = 100
 85        y = 200
 86        radius = 100
 87        mode = 'soft'
 88        color = arcade.csscolor.WHITE
 89        light = Light(x, y, radius, color, mode)
 90        self.light_layer.add(light)
 91
 92        # Create an overlapping, large white light
 93        x = 300
 94        y = 150
 95        radius = 200
 96        color = arcade.csscolor.WHITE
 97        mode = 'soft'
 98        light = Light(x, y, radius, color, mode)
 99        self.light_layer.add(light)
100
101        # Create three, non-overlapping RGB lights
102        x = 50
103        y = 450
104        radius = 100
105        mode = 'soft'
106        color = arcade.csscolor.RED
107        light = Light(x, y, radius, color, mode)
108        self.light_layer.add(light)
109
110        x = 250
111        y = 450
112        radius = 100
113        mode = 'soft'
114        color = arcade.csscolor.GREEN
115        light = Light(x, y, radius, color, mode)
116        self.light_layer.add(light)
117
118        x = 450
119        y = 450
120        radius = 100
121        mode = 'soft'
122        color = arcade.csscolor.BLUE
123        light = Light(x, y, radius, color, mode)
124        self.light_layer.add(light)
125
126        # Create three, overlapping RGB lights
127        x = 650
128        y = 450
129        radius = 100
130        mode = 'soft'
131        color = arcade.csscolor.RED
132        light = Light(x, y, radius, color, mode)
133        self.light_layer.add(light)
134
135        x = 750
136        y = 450
137        radius = 100
138        mode = 'soft'
139        color = arcade.csscolor.GREEN
140        light = Light(x, y, radius, color, mode)
141        self.light_layer.add(light)
142
143        x = 850
144        y = 450
145        radius = 100
146        mode = 'soft'
147        color = arcade.csscolor.BLUE
148        light = Light(x, y, radius, color, mode)
149        self.light_layer.add(light)
150
151        # Create three, overlapping RGB lights
152        # But 'hard' lights that don't fade out.
153        x = 650
154        y = 150
155        radius = 100
156        mode = 'hard'
157        color = arcade.csscolor.RED
158        light = Light(x, y, radius, color, mode)
159        self.light_layer.add(light)
160
161        x = 750
162        y = 150
163        radius = 100
164        mode = 'hard'
165        color = arcade.csscolor.GREEN
166        light = Light(x, y, radius, color, mode)
167        self.light_layer.add(light)
168
169        x = 850
170        y = 150
171        radius = 100
172        mode = 'hard'
173        color = arcade.csscolor.BLUE
174        light = Light(x, y, radius, color, mode)
175        self.light_layer.add(light)
176
177        # Create a light to follow the player around.
178        # We'll position it later, when the player moves.
179        # We'll only add it to the light layer when the player turns the light
180        # on. We start with the light off.
181        radius = 150
182        mode = 'soft'
183        color = arcade.csscolor.WHITE
184        self.player_light = Light(0, 0, radius, color, mode)
185
186        # Create the physics engine
187        self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list)
188
189        # Set the viewport boundaries
190        # These numbers set where we have 'scrolled' to.
191        self.view_left = 0
192        self.view_bottom = 0
193
194    def on_draw(self):
195        """ Draw everything. """
196        arcade.start_render()
197
198        # --- Light related ---
199        # Everything that should be affected by lights gets rendered inside this
200        # 'with' statement. Nothing is rendered to the screen yet, just the light
201        # layer.
202        with self.light_layer:
203            self.background_sprite_list.draw()
204            self.player_list.draw()
205
206        # Draw the light layer to the screen.
207        # This fills the entire screen with the lit version
208        # of what we drew into the light layer above.
209        self.light_layer.draw(ambient_color=AMBIENT_COLOR)
210
211        # Now draw anything that should NOT be affected by lighting.
212        arcade.draw_text("Press SPACE to turn character light on/off.",
213                         10 + self.view_left, 10 + self.view_bottom,
214                         arcade.color.WHITE, 20)
215
216    def on_resize(self, width, height):
217        """ User resizes the screen. """
218
219        # --- Light related ---
220        # We need to resize the light layer to
221        self.light_layer.resize(width, height)
222
223        # Scroll the screen so the user is visible
224        self.scroll_screen()
225
226    def on_key_press(self, key, _):
227        """Called whenever a key is pressed. """
228
229        if key == arcade.key.UP:
230            self.player_sprite.change_y = MOVEMENT_SPEED
231        elif key == arcade.key.DOWN:
232            self.player_sprite.change_y = -MOVEMENT_SPEED
233        elif key == arcade.key.LEFT:
234            self.player_sprite.change_x = -MOVEMENT_SPEED
235        elif key == arcade.key.RIGHT:
236            self.player_sprite.change_x = MOVEMENT_SPEED
237        elif key == arcade.key.SPACE:
238            # --- Light related ---
239            # We can add/remove lights from the light layer. If they aren't
240            # in the light layer, the light is off.
241            if self.player_light in self.light_layer:
242                self.light_layer.remove(self.player_light)
243            else:
244                self.light_layer.add(self.player_light)
245
246    def on_key_release(self, key, _):
247        """Called when the user releases a key. """
248
249        if key == arcade.key.UP or key == arcade.key.DOWN:
250            self.player_sprite.change_y = 0
251        elif key == arcade.key.LEFT or key == arcade.key.RIGHT:
252            self.player_sprite.change_x = 0
253
254    def scroll_screen(self):
255        """ Manage Scrolling """
256
257        # Scroll left
258        left_boundary = self.view_left + VIEWPORT_MARGIN
259        if self.player_sprite.left < left_boundary:
260            self.view_left -= left_boundary - self.player_sprite.left
261
262        # Scroll right
263        right_boundary = self.view_left + self.width - VIEWPORT_MARGIN
264        if self.player_sprite.right > right_boundary:
265            self.view_left += self.player_sprite.right - right_boundary
266
267        # Scroll up
268        top_boundary = self.view_bottom + self.height - VIEWPORT_MARGIN
269        if self.player_sprite.top > top_boundary:
270            self.view_bottom += self.player_sprite.top - top_boundary
271
272        # Scroll down
273        bottom_boundary = self.view_bottom + VIEWPORT_MARGIN
274        if self.player_sprite.bottom < bottom_boundary:
275            self.view_bottom -= bottom_boundary - self.player_sprite.bottom
276
277        # Make sure our boundaries are integer values. While the viewport does
278        # support floating point numbers, for this application we want every pixel
279        # in the view port to map directly onto a pixel on the screen. We don't want
280        # any rounding errors.
281        self.view_left = int(self.view_left)
282        self.view_bottom = int(self.view_bottom)
283
284        arcade.set_viewport(self.view_left,
285                            self.width + self.view_left,
286                            self.view_bottom,
287                            self.height + self.view_bottom)
288
289    def on_update(self, delta_time):
290        """ Movement and game logic """
291
292        # Call update on all sprites (The sprites don't do much in this
293        # example though.)
294        self.physics_engine.update()
295
296        # --- Light related ---
297        # We can easily move the light by setting the position,
298        # or by center_x, center_y.
299        self.player_light.position = self.player_sprite.position
300
301        # Scroll the screen so we can see the player
302        self.scroll_screen()
303
304
305if __name__ == "__main__":
306    window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
307    window.setup()
308    arcade.run()

GPU Particle Burst

_images/explosions.gif

In this example, we show how to create explosions using particles. The particles are tracked by the GPU, significantly improving the performance.

Step 1: Open a Blank Window

First, let’s start with a blank window.

gpu_particle_burst_01.py
 1"""
 2Example showing how to create particle explosions via the GPU.
 3"""
 4import arcade
 5
 6SCREEN_WIDTH = 1024
 7SCREEN_HEIGHT = 768
 8SCREEN_TITLE = "GPU Particle Explosion"
 9
10class MyWindow(arcade.Window):
11    """ Main window"""
12    def __init__(self):
13        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
14
15    def on_draw(self):
16        """ Draw everything """
17        self.clear()
18
19    def on_update(self, dt):
20        """ Update everything """
21        pass
22
23    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
24        """ User clicks mouse """
25        pass
26
27
28if __name__ == "__main__":
29    window = MyWindow()
30    window.center_window()
31    arcade.run()

Step 2: Create One Particle For Each Click

_images/gpu_particle_burst_02.png

For this next section, we are going to draw a dot each time the user clicks her mouse on the screen.

For each click, we are going to create an instance of a “burst” that will eventually be turned into a full explosion. Each burst instance will be added to a list.

Imports

First, we’ll import some more items for our program:

from array import array
from dataclasses import dataclass
import arcade
import arcade.gl
Burst Dataclass

Next, we’ll create a dataclass to track our data for each burst. For each burst we need to track a Vertex Array Object (VAO) which stores information about our burst. Inside of that, we’ll have a Vertex Buffer Object (VBO) which will be a high-speed memory buffer where we’ll store locations, colors, velocity, etc.

@dataclass
class Burst:
    """ Track for each burst. """
    buffer: arcade.gl.Buffer
    vao: arcade.gl.Geometry
Init method

Next, we’ll create an empty list attribute called burst_list. We’ll also create our OpenGL shader program. The program will be a collection of two shader programs. These will be stored in separate files, saved in the same directory.

Note

In addition to loading the program via the load_program() method of ArcadeContext shown, it is also possible to keep the GLSL programs in triple- quoted string by using program() of Context.

MyWindow.__init__
    def __init__(self):
        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
        self.burst_list = []

        # Program to visualize the points
        self.program = self.ctx.load_program(
            vertex_shader="vertex_shader_v1.glsl",
            fragment_shader="fragment_shader.glsl",
        )

        self.ctx.enable_only()
OpenGL Shaders

The OpenGL Shading Language (GLSL) is C-style language that runs on your graphics card (GPU) rather than your CPU. Unfortunately a full explanation of the language is beyond the scope of this tutorial. I hope, however, the tutorial can get you started understanding how it works.

We’ll have two shaders. A vertex shader, and a fragment shader. A vertex shader runs for each vertex point of the geometry we are rendering, and a fragment shader runs for each pixel. For example, vertex shader might run four times for each point on a rectangle, and the fragment shader would run for each pixel on the screen.

The vertex shader takes in the position of our vertex. We’ll set in_pos in our Python program, and pass that data to this shader.

The vertex shader outputs the color of our vertex. Colors are in Red-Green-Blue-Alpha (RGBA) format, with floating-point numbers ranging from 0 to 1. In our program below case, we set the color to (1, 1, 1) which is white, and the fourth 1 for completely opaque.

vertex_shader_v1.glsl
 1#version 330
 2
 3// (x, y) position passed in
 4in vec2 in_pos;
 5
 6// Output the color to the fragment shader
 7out vec4 color;
 8
 9void main() {
10
11    // Set the RGBA color
12    color = vec4(1, 1, 1, 1);
13
14    // Set the position. (x, y, z, w)
15    gl_Position = vec4(in_pos, 0.0, 1);
16}

There’s not much to the fragment shader, it just takes in color from the vertex shader and passes it back out as the pixel color. We’ll use the same fragment shader for every version in this tutorial.

fragment_shader.glsl
 1#version 330
 2
 3// Color passed in from the vertex shader
 4in vec4 color;
 5
 6// The pixel we are writing to in the framebuffer
 7out vec4 fragColor;
 8
 9void main() {
10
11    // Fill the point
12    fragColor = vec4(color);
13}
Mouse Pressed

Each time we press the mouse button, we are going to create a burst at that location.

The data for that burst will be stored in an instance of the Burst class.

The Burst class needs our data buffer. The data buffer contains information about each particle. In this case, we just have one particle and only need to store the x, y of that particle in the buffer. However, eventually we’ll have hundreds of particles, each with a position, velocity, color, and fade rate. To accommodate creating that data, we have made a generator function _gen_initial_data. It is totally overkill at this point, but we’ll add on to it in this tutorial.

The buffer_description says that each vertex has two floating data points (2f) and those data points will come into the shader with the reference name in_pos which we defined above in our OpenGL Shaders

MyWindow.on_mouse_press
    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
        """ User clicks mouse """

        def _gen_initial_data(initial_x, initial_y):
            """ Generate data for each particle """
            yield initial_x
            yield initial_y

        # Recalculate the coordinates from pixels to the OpenGL system with
        # 0, 0 at the center.
        x2 = x / self.width * 2. - 1.
        y2 = y / self.height * 2. - 1.

        # Get initial particle data
        initial_data = _gen_initial_data(x2, y2)

        # Create a buffer with that data
        buffer = self.ctx.buffer(data=array('f', initial_data))

        # Create a buffer description that says how the buffer data is formatted.
        buffer_description = arcade.gl.BufferDescription(buffer,
                                                         '2f',
                                                         ['in_pos'])
        # Create our Vertex Attribute Object
        vao = self.ctx.geometry([buffer_description])

        # Create the Burst object and add it to the list of bursts
        burst = Burst(buffer=buffer, vao=vao)
        self.burst_list.append(burst)
Drawing

Finally, draw it.

MyWindow.on_draw
    def on_draw(self):
        """ Draw everything """
        self.clear()

        # Set the particle size
        self.ctx.point_size = 2 * self.get_pixel_ratio()

        # Loop through each burst
        for burst in self.burst_list:

            # Render the burst
            burst.vao.render(self.program, mode=self.ctx.POINTS)
Program Listings

Step 3: Multiple Moving Particles

_images/gpu_particle_burst_03.png

Next step is to have more than one particle, and to have the particles move. We’ll do this by creating the particles, and calculating where they should be based on the time since creation. This is a bit different than the way we move sprites, as they are manually repositioned bit-by-bit during each update call.

Imports

First, we’ll import both the random and time libraries:

import random
import time
Constants

Then we need to create a constant that contains the number of particles to create:

PARTICLE_COUNT = 300
Burst Dataclass

We’ll need to add a time to our burst data. This will be a floating point number that represents the start-time of when the burst was created.

@dataclass
class Burst:
    """ Track for each burst. """
    buffer: arcade.gl.Buffer
    vao: arcade.gl.Geometry
    start_time: float
Update Burst Creation

Now when we create a burst, we need multiple particles, and each particle also needs a velocity. In _gen_initial_data we add a loop for each particle, and also output a delta x and y.

Note: Because of how we set delta x and delta y, the particles will expand into a rectangle rather than a circle. We’ll fix that on a later step.

Because we added a velocity, our buffer now needs two pairs of floats 2f 2f named in_pos and in_vel. We’ll update our shader in a bit to work with the new values.

Finally, our burst object needs to track the time we created the burst.

 1    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
 2        """ User clicks mouse """
 3
 4        def _gen_initial_data(initial_x, initial_y):
 5            """ Generate data for each particle """
 6            for i in range(PARTICLE_COUNT):
 7                dx = random.uniform(-.2, .2)
 8                dy = random.uniform(-.2, .2)
 9                yield initial_x
10                yield initial_y
11                yield dx
12                yield dy
13
14        # Recalculate the coordinates from pixels to the OpenGL system with
15        # 0, 0 at the center.
16        x2 = x / self.width * 2. - 1.
17        y2 = y / self.height * 2. - 1.
18
19        # Get initial particle data
20        initial_data = _gen_initial_data(x2, y2)
21
22        # Create a buffer with that data
23        buffer = self.ctx.buffer(data=array('f', initial_data))
24
25        # Create a buffer description that says how the buffer data is formatted.
26        buffer_description = arcade.gl.BufferDescription(buffer,
27                                                         '2f 2f',
28                                                         ['in_pos', 'in_vel'])
29        # Create our Vertex Attribute Object
30        vao = self.ctx.geometry([buffer_description])
31
32        # Create the Burst object and add it to the list of bursts
33        burst = Burst(buffer=buffer, vao=vao, start_time=time.time())
34        self.burst_list.append(burst)
Set Time in on_draw

When we draw, we need to set “uniform data” (data that is the same for all points) that says how many seconds it has been since the burst started. The shader will use this to calculate particle position.

    def on_draw(self):
        """ Draw everything """
        self.clear()

        # Set the particle size
        self.ctx.point_size = 2 * self.get_pixel_ratio()

        # Loop through each burst
        for burst in self.burst_list:

            # Set the uniform data
            self.program['time'] = time.time() - burst.start_time

            # Render the burst
            burst.vao.render(self.program, mode=self.ctx.POINTS)
Update Vertex Shader

Our vertex shader needs to be updated. We now take in a uniform float called time. Uniform data is set once, and each vertex in the program can use it. In our case, we don’t need a separate copy of the burst’s start time for each particle in the burst, therefore it is uniform data.

We also need to add another vector of two floats that will take in our velocity. We set in_vel in Update Burst Creation.

Then finally we calculate a new position based on the time and our particle’s velocity. We use that new position when setting gl_Position.

vertex_shader_v2.glsl
 1#version 330
 2
 3// Time since burst start
 4uniform float time;
 5
 6// (x, y) position passed in
 7in vec2 in_pos;
 8
 9// Velocity of particle
10in vec2 in_vel;
11
12// Output the color to the fragment shader
13out vec4 color;
14
15void main() {
16
17    // Set the RGBA color
18    color = vec4(1, 1, 1, 1);
19
20    // Calculate a new position
21    vec2 new_pos = in_pos + (time * in_vel);
22
23    // Set the position. (x, y, z, w)
24    gl_Position = vec4(new_pos, 0.0, 1);
25}
Program Listings

Step 4: Random Angle and Speed

_images/gpu_particle_burst_04.png

Step 3 didn’t do a good job of picking a velocity, as our particles expanded into a rectangle rather than a circle. Rather than just pick a random delta x and y, we need to pick a random direction and speed. Then calculate delta x and y from that.

Update Imports

Import the math library so we can do some trig:

import math
Update Burst Creation

Now, pick a random direction from zero to 2 pi radians. Also, pick a random speed. Then use sine and cosine to calculate the delta x and y.

 1    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
 2        """ User clicks mouse """
 3
 4        def _gen_initial_data(initial_x, initial_y):
 5            """ Generate data for each particle """
 6            for i in range(PARTICLE_COUNT):
 7                angle = random.uniform(0, 2 * math.pi)
 8                speed = random.uniform(0.0, 0.3)
 9                dx = math.sin(angle) * speed
10                dy = math.cos(angle) * speed
11                yield initial_x
12                yield initial_y
13                yield dx
14                yield dy
15
Program Listings

Step 5: Gaussian Distribution

_images/gpu_particle_burst_05.png

Setting speed to a random amount makes for an expanding circle. Another option is to use a gaussian function to produce more of a ‘splat’ look:

                speed = abs(random.gauss(0, 1)) * .5
Program Listings

Step 6: Add Color

_images/gpu_particle_burst_06.png

So far our particles have all been white. How do we add in color? We’ll need to generate it for each particle. Shaders take colors in the form of RGB floats, so we’ll generate a random number for red, and add in some green to get our yellows. Don’t add more green than red, or else you get a green tint.

Finally, pass in the three floats as in_color to the shader buffer (VBO).

 1    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
 2        """ User clicks mouse """
 3
 4        def _gen_initial_data(initial_x, initial_y):
 5            """ Generate data for each particle """
 6            for i in range(PARTICLE_COUNT):
 7                angle = random.uniform(0, 2 * math.pi)
 8                speed = abs(random.gauss(0, 1)) * .5
 9                dx = math.sin(angle) * speed
10                dy = math.cos(angle) * speed
11                red = random.uniform(0.5, 1.0)
12                green = random.uniform(0, red)
13                blue = 0
14                yield initial_x
15                yield initial_y
16                yield dx
17                yield dy
18                yield red
19                yield green
20                yield blue
21
22        # Recalculate the coordinates from pixels to the OpenGL system with
23        # 0, 0 at the center.
24        x2 = x / self.width * 2. - 1.
25        y2 = y / self.height * 2. - 1.
26
27        # Get initial particle data
28        initial_data = _gen_initial_data(x2, y2)
29
30        # Create a buffer with that data
31        buffer = self.ctx.buffer(data=array('f', initial_data))
32
33        # Create a buffer description that says how the buffer data is formatted.
34        buffer_description = arcade.gl.BufferDescription(buffer,
35                                                         '2f 2f 3f',
36                                                         ['in_pos', 'in_vel', 'in_color'])
37        # Create our Vertex Attribute Object
38        vao = self.ctx.geometry([buffer_description])
39
40        # Create the Burst object and add it to the list of bursts
41        burst = Burst(buffer=buffer, vao=vao, start_time=time.time())
42        self.burst_list.append(burst)

Then, update the shader to use the color instead of always using white:

vertex_shader_v3.glsl
 1#version 330
 2
 3// Time since burst start
 4uniform float time;
 5
 6// (x, y) position passed in
 7in vec2 in_pos;
 8
 9// Velocity of particle
10in vec2 in_vel;
11
12// Color of particle
13in vec3 in_color;
14
15// Output the color to the fragment shader
16out vec4 color;
17
18void main() {
19
20    // Set the RGBA color
21    color = vec4(in_color[0], in_color[1], in_color[2], 1);
22
23    // Calculate a new position
24    vec2 new_pos = in_pos + (time * in_vel);
25
26    // Set the position. (x, y, z, w)
27    gl_Position = vec4(new_pos, 0.0, 1);
28}
Program Listings

Step 7: Fade Out

_images/gpu_particle_burst_07.png

Right now the explosion particles last forever. Let’s get them to fade out. Once a burst has faded out, let’s remove it from burst_list.

Constants

First, let’s add a couple constants to control the minimum and maximum tile to fade a particle:

MIN_FADE_TIME = 0.25
MAX_FADE_TIME = 1.5
Update Init

Next, we need to update our OpenGL context to support alpha blending. Go back to the __init__ method and update the enable_only call to:

self.ctx.enable_only(self.ctx.BLEND)
Add Fade Rate to Buffer

Next, add the fade rate to the VBO:

 1    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
 2        """ User clicks mouse """
 3
 4        def _gen_initial_data(initial_x, initial_y):
 5            """ Generate data for each particle """
 6            for i in range(PARTICLE_COUNT):
 7                angle = random.uniform(0, 2 * math.pi)
 8                speed = abs(random.gauss(0, 1)) * .5
 9                dx = math.sin(angle) * speed
10                dy = math.cos(angle) * speed
11                red = random.uniform(0.5, 1.0)
12                green = random.uniform(0, red)
13                blue = 0
14                fade_rate = random.uniform(1 / MAX_FADE_TIME, 1 / MIN_FADE_TIME)
15
16                yield initial_x
17                yield initial_y
18                yield dx
19                yield dy
20                yield red
21                yield green
22                yield blue
23                yield fade_rate
24
25        # Recalculate the coordinates from pixels to the OpenGL system with
26        # 0, 0 at the center.
27        x2 = x / self.width * 2. - 1.
28        y2 = y / self.height * 2. - 1.
29
30        # Get initial particle data
31        initial_data = _gen_initial_data(x2, y2)
32
33        # Create a buffer with that data
34        buffer = self.ctx.buffer(data=array('f', initial_data))
35
36        # Create a buffer description that says how the buffer data is formatted.
37        buffer_description = arcade.gl.BufferDescription(buffer,
38                                                         '2f 2f 3f f',
39                                                         ['in_pos',
40                                                          'in_vel',
41                                                          'in_color',
42                                                          'in_fade_rate'])
43        # Create our Vertex Attribute Object
44        vao = self.ctx.geometry([buffer_description])
45
46        # Create the Burst object and add it to the list of bursts
47        burst = Burst(buffer=buffer, vao=vao, start_time=time.time())
48        self.burst_list.append(burst)
Update Shader

Update the shader. Calculate the alpha. If it is less that 0, just use 0.

vertex_shader_v4.glsl
 1#version 330
 2
 3// Time since burst start
 4uniform float time;
 5
 6// (x, y) position passed in
 7in vec2 in_pos;
 8
 9// Velocity of particle
10in vec2 in_vel;
11
12// Color of particle
13in vec3 in_color;
14
15// Fade rate
16in float in_fade_rate;
17
18// Output the color to the fragment shader
19out vec4 color;
20
21void main() {
22
23    // Calculate alpha based on time and fade rate
24    float alpha = 1.0 - (in_fade_rate * time);
25    if(alpha < 0.0) alpha = 0;
26
27    // Set the RGBA color
28    color = vec4(in_color[0], in_color[1], in_color[2], alpha);
29
30    // Calculate a new position
31    vec2 new_pos = in_pos + (time * in_vel);
32
33    // Set the position. (x, y, z, w)
34    gl_Position = vec4(new_pos, 0.0, 1);
35}
Remove Faded Bursts

Once our burst has completely faded, no need to keep it around. So in our on_update remove the burst from the burst_list after it has been faded.

 1    def on_update(self, dt):
 2        """ Update game """
 3
 4        # Create a copy of our list, as we can't modify a list while iterating
 5        # it. Then see if any of the items have completely faded out and need
 6        # to be removed.
 7        temp_list = self.burst_list.copy()
 8        for burst in temp_list:
 9            if time.time() - burst.start_time > MAX_FADE_TIME:
10               self.burst_list.remove(burst)
Program Listings

Step 8: Add Gravity

You could also add come gravity to the particles by adjusting the velocity based on a gravity constant. (In this case, 1.1.)

// Adjust velocity based on gravity
vec2 new_vel = in_vel;
new_vel[1] -= time * 1.1;

// Calculate a new position
vec2 new_pos = in_pos + (time * new_vel);
Program Listings

Bundling a Game with PyInstaller

Note

You must have Arcade version 2.4.3 or greater and Pymunk 5.7.0 or greater for the instructions below to work.

You’ve written your game using Arcade and it is a masterpiece! Congrats! Now you want to share it with others. That usually means helping people install Python, downloading the necessary modules, copying your code, and then getting it all working. Sharing is not an easy task. Well, PyInstaller can change all that!

PyInstaller is a tool for Python that lets you bundle up an entire Python application into a one-file executable bundle that you can easily share. Thankfully, it works great with Arcade!

Bundling a Simple Arcade Script

To demonstrate how PyInstaller works, we will:

  • install PyInstaller

  • create a simple example application that uses Arcade

  • bundle the application into a one-file executable

  • run the application

First, make sure both Arcade and PyInstaller are installed in your Python environment with:

pip install arcade pyinstaller

Then create a file called myscript.py that contains the following:

import arcade
arcade.open_window(400, 400, "My Script")
arcade.start_render()
arcade.draw_circle_filled(200, 200, 100, arcade.color.BLUE)
arcade.finish_render()
arcade.run()

Now, create a one-file executable bundle file by running PyInstaller:

pyinstaller myscript.py --onefile

PyInstaller generates the executable that is a bundle of your game. It puts it in the dist\ folder under your current working directory. Look for a file named myscript.exe in dist\. Run this and see the example application start up!

You can copy this file wherever you want on your computer and run it. Or, share it with others. Everything your script needs is inside this executable file.

For simple games, this is all you need to know! But, if your game loads any kind of data files from disk, continue reading.

Handling Data Files

When creating a bundle, PyInstaller first examines your project and automatically identifies nearly everything your project needs (a Python interpreter, installed modules, etc). But, it can’t automatically determine what data files your game is loading from disk (images, sounds, maps). So, you must explicitly tell PyInstaller about these files and where it should put them in the bundle. This is done with PyInstaller’s --add-data flag:

pyinstaller myscript.py --add-data "stripes.jpg;."

The first item passed to --add-data is the “source” file or directory (ex: stripes.jpg) identifying what PyInstaller should include in the bundle. The item after the semicolon is the “destination” (ex: “.”), which specifies where files should be placed in the bundle, relative to the bundle’s root. In the example above, the stripes.jpg image is copied to the root of the bundle (“.”).

After instructing PyInstaller to include data files in a bundle, you must make sure your code loads the data files from the correct directory. When you share your game’s bundle, you have no control over what directory the user will run your bundle from. This is complicated by the fact that a one-file PyInstaller bundle is uncompressed at runtime to a random temporary directory and then executed from there. This document describes one simple approach that allows your code to execute and load files when running in a PyInstaller bundle AND also be able to run when not bundled.

You need to do two things. First, the snippet below must be placed at the beginning of your script:

if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
    os.chdir(sys._MEIPASS)

This snippet uses sys.frozen and sys._MEIPASS, which are both set by PyInstaller. The sys.frozen setting indicates whether code is running from a bundle (“frozen”). If the code is “frozen”, the working directory is changed to the root of where the bundle has been uncompressed to (sys._MEIPASS). PyInstaller often uncompresses its one-file bundles to a directory named something like: C:\Users\user\AppData\Local\Temp\_MEI123456.

Second, once the code above has set the current working directory, all file paths in your code can be relative paths (ex: resources\images\stripes.jpg) as opposed to absolute paths (ex: C:\projects\mygame\resources\images\stripes.jpg). If you do these two things and add data files to your package as demonstrated below, your code will be able to run “normally” as well as running in a bundle.

Below are some examples that show a few common patterns of how data files can be included in a PyInstaller bundle. The examples first show a code snippet that demonstrates how data is loaded (relative path names), followed by the PyInstaller command to copy data files into the bundle. They all assume that the os.chdir() snippet of code listed above is being used.

One Data File

If you simply have one data file in the same directory as your script, refer to the data file using a relative path like this:

sprite = arcade.Sprite("stripes.jpg")

Then, you would use a PyInstaller command like this to include the data file in the bundled executable:

pyinstaller myscript.py --add-data "stripes.jpg;."
...or...
pyinstaller myscript.py --add-data "*.jpg;."
One Data Directory

If you have a directory of data files (such as images), refer to the data directory using a relative path like this:

sprite = arcade.Sprite("images/player.jpg")
sprite = arcade.Sprite("images/enemy.jpg")

Then, you would use a PyInstaller command like this to include the directory in the bundled executable:

pyinstaller myscript.py --add-data "images;images"
Multiple Data Files and Directories

You can use the --add-data flag multiple times to add multiple files and directories into the bundle:

pyinstaller myscript.py --add-data "player.jpg;." --add-data "enemy.jpg;." --add-data "music;music"
One Directory for Everything

Although you can include every data file and directory with separate --add-data flags, it is suggested that you write your game so that all of your data files are under one root directory, often named resources. You can use subdirectories to help organize everything. An example directory tree could look like:

project/
|--- game.py
|--- resources/
     |--- images/
     |    |--- enemy.jpg
     |    |--- player.jpg
     |--- sound/
     |    |--- game_over.wav
     |    |--- laser.wav
     |--- text/
          |--- names.txt

With this approach, it becomes easy to bundle all your data with just a single --add-data flag. Your code would use relative pathnames to load resources, something like this:

sprite = arcade.Sprite("resources/images/player.jpg")
text = open("resources/text/names.txt").read()

And, you would include this entire directory tree into the bundle like this:

pyinstaller myscript.py --add-data "resources;resources"

It is worth spending a bit of time to plan out how you will layout and load your data files in order to keep the bundling process simple.

The technique of handling data files described above is just one approach. If you want more control and flexibility in handling data files, learn about the different path information that is available by reading the PyInstaller Run-Time Information documentation.

Now that you know how to install PyInstaller, include data files, and bundle your game into an executable, you have what you need to bundle your game and share it with your new fans!

Troubleshooting

Use a One-Folder Bundle for Troubleshooting

If you are having problems getting your bundle to work properly, it may help to temporarily omit the --onefile flag from the pyinstaller command. This will bundle your game into a one-folder bundle with an executable inside it. This allows you to inspect the contents of the folder and make sure all of the files are where you expect them to be. The one-file bundle produced by --onefile is simply a self-uncompressing archive of this one-folder bundle.

PyInstaller Not Bundling a Needed Module

In most cases, PyInstaller is able to analyze your project and automatically determine what modules to place in the bundle. But, if PyInstaller happens to miss a module, you can use the --hidden-import MODULENAME flag to explicitly instruct PyInstaller to include a module. See the PyInstaller documentation for more details.

Extra Details

  • You will notice that after running pyinstaller, a .spec file will appear in your directory. This file is generated by PyInstaller and does not need to be saved or checked into your source code repo.

  • Executable one-file bundles produced by PyInstaller’s --onefile flag will start up slower than your original application or the one-folder bundle. This is expected because one-file bundles are ultimately just a compressed folder, so they must take time to uncompress themselves each time the bundle is run.

  • By default, when PyInstaller creates a bundled application, the application opens a console window. You can suppress the creation of the console window by adding the --windowed flag to the pyinstaller command.

  • See the PyInstaller documentation below for more details on the topics above, and much more.

PyInstaller Documentation

PyInstaller is a flexible tool that can handle a wide variety of different situations. For further reading, here are links to the official PyInstaller documentation:

Diverse Coders

If you are female, trans, or non-binary, there are a lot of coders like you! But sometimes it can be hard to find them.

Check out the following organizations.

PyLadies holds charity auctions at PyCon every year, raising over $20,000 every year to help support and promote coding amongst women.

If you are interested in getting started coding, or contributing to the Python community, reach out to me or any of the organizations above.

Social Media

Release Notes

Keep up-to-date with the latest changes to the Arcade library by the release notes.

Version 2.5.7

Fixes
  • The arcade gui should now respect the current viewport

  • Fixed an issue with UILabel allocating large amounts of textures over time consuming a lot of memory

  • Fixed an issue with the initial viewport sometimes being 1 pixel too small causing some artifacts

  • Fixed a race condition in Sound.stop() sometimes causing a crash

  • Fixed an issue in requirements causing issues for poetry

  • Fixed an error reporting issue when reaching maximum texture size

New Features

replit.com

Arcade should now work out of the box on replit.com. We detect when arcade runs in replit tweaking various settings. One important setting we disable is antialiasing since this doesn’t work well with software rendering.

Alternative Garbage Collection of OpenGL Resources

arcade.gl.Context now supports an alternative garbage collection mode more compatible with threaded applications and garbage collection of OpenGL resources. OpenGL resources can only be accessed or destroyed from the same thread the window was created. In threaded applications the python garbage collector can in some cases try to destroy OpenGL objects possibly causing a hard crash.

This can be configured when creating the arcade.Window passing in a new gc_mode parameter. By default his parameter is "auto" providing the default garbage collection we have in python.

Passing in "context_gc" on the other hand will move all “dead” OpenGL objects into Context.objects. These can be garbage collected manually by calling Context.gc() in a more controlled way in the the right thread.

Version 2.5.6

Version 2.5.6 was released 2021-03-28

  • Fix issue with PyInstaller and Pymunk not allowing Arcade to work with bundling

  • Fix some PyMunk examples

  • Update some example code. Highlight PyInstaller instructions

Version 2.5.5

Version 2.5.5 was released 2021-02-23

Version 2.5.4

Version 2.5.4 was released 2021-02-19

Version 2.5.3

Version 2.5.3 was released 2021-01-27

Version 2.5.2

Version 2.5.2 was released 2020-12-27

  • Improve schedule/unschedule docstrings

  • Fix Sound.get_length

  • Raise error if there are multiple instances of a streaming source

  • Fix background music example to match new sound API

  • Update main landing page for docs

  • Split sprite platformer tutorial into multiple pages

  • Add ‘related projects’ page

  • Add ‘adventure’ sample game link

  • Add resources for top-down tank images

  • Add turn-and-move example

  • Fix name of sandCorner_left.png

  • Update tilemap to error out instead of continuing if we can’t find a tile

  • Improve view tutorial

  • Generate error rather than warning if we can’t find image or sound file

  • Specify timer resolution in Windows

Version 2.5.1

Version 2.5.1 was released 2020-12-14

Version 2.5

Version 2.5 was released 2020-12-09

(Note, libraries Arcade depends on do not work yet with Python 3.9 on Mac. Mac users will need to use Python 3.6, 3.7 or 3.8.)

General

  • SpriteList.draw now supports a blend_function parameter. This opens up for drawing sprites with different blend modes.

  • Bugfix: Sprite hit box didn’t properly update when changing width or height

  • GUI improvements (eruvanos needs to elaborate)

  • Several examples was improved

  • Improvements to the pyinstaller tutorial

  • Better pin versions of depended libraries

  • Fix issues with simple and platformer physics engines.

Advanced

  • Added support for tessellation shaders

  • arcade.Window now takes a gl_version parameter so users can request a higher OpenGL version than the default (3, 3) version. This only be used to advanced users.

  • Bugfix: Geometry’s internal vertex count was incorrect when using an index buffer

  • We now support 8, 16 and 32 bit index buffers

  • Optimized several draw methods by omitting tobytes() letting the buffer protocol do the work

  • More advanced examples was added to arcade/experimental/examples

Documentation

Version 2.4.3

Version 2.4.3 was released 2020-09-30

General

  • Added PyInstalled hook and tutorial

  • ShapeLists should no longer share position between instances

  • GUI improvements: new UIImageToggle

Low level rendering api (arcade.gl):

  • ArcadeContext now has a load_texture method for creating opengl textures using Pillow.

  • Bug: Fixed an issue related to drawing indexed geometry with offset

  • Bug: Scissor box not updating when using framebuffer

  • Bug: Fixed an issue with pack/unpack alignment for textures

  • Bug: Transforming geometry into a target buffer should now work with byte offset

  • Bug: Duplicate sprites in ‘check_for_collision_with_list’ Issue #763

  • Improved docstrings in arcade.gl

Version 2.4.2

Version 2.4.2 was released 2020-09-08

  • Enhancement: draw_hit_boxes new method in SpriteList.

  • Enhancement: draw_points now significantly faster

  • Added UIToggle, on/off switch

  • Add example showing how to do GPU transformations with the mouse

  • Create buttons with default size/position so size can be set after creation.

  • Allow checking if a sound is done playing Issue 728

  • Add an early camera mock-up

  • Add finish method to arcade.gl.context.

  • New example arcade.experimental.examples.3d_cube (experimental)

  • New example arcade.examples.camera_example

  • Improved UIManager.unregister_handlers(), improves multi view setup

  • Update preload_textures method of SpriteList to actually pre-load textures

  • GUI code clean-up Issue 723

  • Update downloadable .zip for for platformer example code to match current code in documentation.

  • Bug Fix: draw_point calculates wrong point size

  • Fixed draw_points calculates wrong point size

  • Fixed create_line_loop for thickness !=

  • Fixed pixel scale for offscreen framebuffers and read()

  • Fixed SpriteList iterator is stateful

  • Fix for pixel scale in offscreen framebuffers

  • Fix for UI tests

  • Fix issues with FBO binding

  • Cleanup Remove old examples and code

Version 2.4

Arcade 2.4.1 was released 2020-07-13.

Arcade version 2.4 is a major enhancement release to Arcade.

_images/light_demo1.png _images/astar_pathfinding.png _images/mini_map_defender1.png _images/bloom_defender.png _images/gui_elements.png _images/title_animated_gif.gif _images/explosions.gif _images/animated.gif examples/transform_feedback.gif
Version 2.4 Major Features
Version 2.4 Minor Features

New functions/classes:

  • Added get_display_size() to get resolution of the monitor

  • Added Window.center_window() to center the window on the monitor.

  • Added has_line_of_sight() to calculate if there is line-of-sight between two points.

  • Added SpriteSolidColor class that makes a solid-color rectangular sprite.

  • Added SpriteCircle class that makes a circular sprite, either solid or with a fading gradient.

  • Added get_distance function to get the distance between two points.

New functionality:

  • Support for logging. See Logging.

  • Support volume and pan arguments in play_sound

  • Add ability to directly assign items in a sprite list. This is particularly useful when re-ordering sprites for drawing.

  • Support left/right/rotated sprites in tmx maps generated by the Tiled Map Editor.

  • Support getting tmx layer by path, making it less likely reading in a tmx file will have directory confusion issues.

  • Add in font searching code if we can’t find default font when drawing text.

  • Added arcade.Sprite.draw_hit_box method to draw a hit box outline.

  • The arcade.Texture class, arcade.Sprite class, and arcade.tilemap.process_layer take in hit_box_algorithm and hit_box_detail parameters for hit box calculation.

_images/hit_box_algorithm_none.png

hit_box_algorithm = “None”

_images/hit_box_algorithm_simple.png

hit_box_algorithm = “Simple”

_images/hit_box_algorithm_detailed.png

hit_box_algorithm = “Detailed”

Version 2.4 Under-the-hood improvements

General

  • Simple Physics engine is less likely to ‘glitch’ out.

  • Anti-aliasing should now work on windows if antialiasing=True is passed in the window constructor.

  • Major speed improvements to drawing of shape primitives, such as lines, squares, and circles by moving more of the work to the graphics processor.

  • Speed improvements for sprites including gpu-based sprite culling (don’t draw sprites outside the screen).

  • Speed improvements due to shader caching. This should be especially noticeable on Mac OS.

  • Speed improvements due to more efficient ways of setting rendering states such as projection.

  • Speed improvements due to less memory copying in the lower level rendering API.

OpenGL API

A brand new low level rendering API wrapping OpenGL 3.3 core was added in this release. It’s loosely based on the ModernGL API, so ModernGL users should be able to pick it up fast. This API is used by arcade for all the higher level drawing functionality, but can also be used by end users to really take advantage of their GPU. More guides and tutorials around this is likely to appear in the future.

A simplified list of features in the new API:

  • A Context and arcade.ArcadeContext object was introduced and can be found through the window.ctx property. This object offers methods to create opengl resources such as textures, programs/shaders, framebuffers, buffers and queries. It also has shortcuts for changing various context states. When working with OpenGL in arcade you are encouraged to use arcade.gl instead of pyglet.gl. This is important as the context is doing quite a bit of bookkeeping to make our life easier.

  • New Texture class supporting a wide variety of formats such as 8/16/32 bit integer, unsigned integer and float values. New convenient methods and properties was also added to change filtering, repeat mode, read and write data, building mipmaps etc.

  • New Buffer class with methods for manipulating data such as simple reading/writing and copying data from other buffers. This buffer can also now be bound as a uniform buffer object.

  • New Framebuffer wrapper class making us able to render any content into one more more textures. This opens up for a lot of possibilities.

  • The Program has been expanded to support geometry shaders and transform feedback (rendering to a buffer instead of a screen). It also exposes a lot of new properties due to much more details introspection during creation. We also able to assign binding locations for uniform blocks.

  • A simple glsl wrapper/parser was introduced to sanity check the glsl code, inject preprocessor values and auto detect out attributes (used in transforms).

  • A higher level type Geometry was introduced to make working with shaders/programs a lot easier. It supports using a subset of attributes defined in your buffer description by inspecting the the program’s attributes generating and caching compatible variants internally.

  • A Query class was added for easy access to low level measuring of opengl rendering calls. We can get the number samples written, number of primitives processed and time elapsed in nanoseconds.

  • Added support for the buffer protocol. When arcade.gl requires byte data we can also pass objects like numpy array of pythons array.array directly not having to convert this data to bytes.

Version 2.4 New Documentation
Version 2.4 ‘Experimental’

There is now an arcade.experimental module that holds code still under development. Any code in this module might still have API changes.

Special Thanks

Special thanks to Einar Forselv and Maic Siemering for their significant work in helping put this release together.

Version 2.3.15

Release Date: Apr-14-2020

  • Bug Fix: Fix invalid empty text width Issue 633

  • Bug Fix: Make sure file name is string before checking resources Issue 636

  • Enhancement: Implement Size and Rotation for Tiled Objects Issue 638

  • Documentation: Fix incorrect link to ‘sprites following player’ example

Version 2.3.14

Release Date: Apr-9-2020

  • Bug Fix: Another attempt at fixing sprites with different dimensions added to same SpriteList didn’t display correctly Issue 630

  • Add lots of unit tests around Sprites and texture loading.

Version 2.3.13

Release Date: Apr-8-2020

  • Bug Fix: Sprites with different dimensions added to same SpriteList didn’t display correctly Issue 630

Version 2.3.12

Release Date: Apr-8-2020

  • Enhancement: Support more textures in a SpriteList Issue 332

Version 2.3.11

Release Date: Apr-5-2020

  • Bug Fix: Fix procedural_caves_bsp.py

  • Bug Fix: Improve Windows install docs Issue 623

Version 2.3.10

Release Date: Mar-31-2020

  • Bug Fix: Remove unused AudioStream and PlaysoundException from __init__

  • Remove attempts to load ffmpeg library

  • Add background music example

  • Bug Fix: Improve Windows install docs Issue 619

  • Add tutorial on edge artifacts Issue 418

  • Bug Fix: Can’t remove sprite from multiple lists Issue 621

  • Several documentation updates

Version 2.3.9

Release Date: Mar-25-2020

  • Bug Fix: Fix for calling SpriteList.remove Issue 613

  • Bug Fix: get_image not working correctly on hi-res macs Issue 594

  • Bug Fix: Fix for “shiver” in simple physics engine Issue 614

  • Bug Fix: Fix for create_line_strip Issue 616

  • Bug Fix: Fix for volume control Issue 610

  • Bug Fix: Fix for loading SoLoud under Win64 Issue 615

  • Fix jumping/falling texture in platformer example

  • Add tests for gui.theme Issue 605

  • Fix bad link to arcade.color docs

Version 2.3.8

Release Date: Mar-09-2020

  • Major enhancement to sound. Uses SoLoud cross-platform library. New features include support for sound volume, sound stop, and pan left/right.

Version 2.3.7

Release Date: Feb-27-2020

  • Bug Fix: If setting color of sprite with 4 ints, also set alpha

  • Enhancement: Add image for code page 437

  • Bug Fix: Fixes around hit box calcs Issue 601

  • Bug Fix: Fixes for animated tiles and loading animated tiles from tile maps Issue 603

Version 2.3.6

Release Date: Feb-17-2020

  • Enhancement: Add texture transformations Issue 596

  • Bug Fix: Fix off-by-one issue with default viewport

  • Bug Fix: Arcs are drawn double-sized Issue 598

  • Enhancement: Add get_sprites_at_exact_point function

  • Enhancement: Add code page 437 to default resources

Version 2.3.5

Release Date: Feb-12-2020

  • Bug Fix: Calling sprite.draw wasn’t drawing the sprite if scale was 1 Issue 575

  • Add unit test for Issue 575

  • Bug Fix: Changing sprite scale didn’t cause sprite to redraw in new scale Issue 588

  • Add unit test for Issue 588

  • Enhancement: Simplify using built-in resources Issue 576

  • Fix for failure on on_resize(), which pyglet was quietly ignoring

  • Update rotate_point function to make it more obvious it takes degrees

Version 2.3.4

Release Date: Feb-08-2020

  • Bug Fix: Sprites weren’t appearing Issue 585

Version 2.3.3

Release Date: Feb-08-2020

  • Bug Fix: set_scale checks height rather than scale Issue 578

  • Bug Fix: Window flickers for drawing when not derived from Window class Issue 579

  • Enhancement: Allow joystick selection in dual-stick shooter Issue 571

  • Test coverage reporting now working correctly with TravisCI

  • Improved test coverage

  • Improved documentation and typing with Texture class

  • Improve minimal View example

Version 2.3.2

Release Date: Feb-01-2020

  • Remove scale as a parameter to load_textures because it is not unused

  • Improve documentation

  • Add example for acceleration/friction

Version 2.3.1

Release Date: Jan-30-2020

  • Don’t auto-update sprite hit box with animated sprite

  • Fix issues with sprite.draw

  • Improve error message given when trying to do a collision check and there’s no hit box set on the sprite.

Version 2.3.0

Release Date: Jan-30-2020

  • Backwards Incompatability: arcade.Texture no longer has a scale property. This property made things confusing as Sprites had their own scale attribute. This seemingly small change required a lot of rework around sprites, sprite lists, hit boxes, and drawing of textured rectangles.

  • Include all the things that were part of 2.2.8, but hopefully working now.

  • Bug Fix: Error when calling Sprite.draw() Issue 570

  • Enhancement: Added Sprite.draw_hit_box to visually draw the hit box. (Kind of slow, but useful for debugging.)

Version 2.2.9

Release Date: Jan-28-2020

  • Roll back to 2.2.7 because bug fixes in 2.2.8 messed up scaling

Version 2.2.8

Release Date: Jan-27-2020

  • Version number now contained in one file, rather than three.

  • Enhancement: Move several GitHub-listed enhancements to the .rst enhancement list

  • Bug Fix: Texture scale not accounted for when getting height Issue 516

  • Bug Fix: Issue with text cut off if it goes below baseline Issue 515

  • Enhancement: Allow non-cached texture creation, fixing issue with resizing Issue 506

  • Enhancement: Physics engine supports rotation

  • Bug Fix: Need to better resolve collisions so sprite doesn’t get hyper-spaces to new weird spot Issue 569

  • Bug Fix: Hit box not getting properly created when working with multi-texture player sprite. Issue 568

  • Bug Fix: Issue with text_sprite and anchor y of top Issue 567

  • Bug Fix: Issues with documentation

Version 2.2.7

Release Date: Jan-25-2020

  • Enhancement: Have draw_text return a sprite Issue 565

  • Enhancement: Improve speed when changing alpha of text Issue 563

  • Enhancement: Add dual-stick shooter example Issue 301

  • Bug Fix: Fix for Pyglet 2.0dev incompatability Issue 560

  • Bug Fix: Fix broken particle_systems.py example Issue 558

  • Enhancement: Added mypy check to TravisCI build Issue 557

  • Enhancement: Fix typing issues Issue 537

  • Enhancement: Optimize load font in draw_text Issue 525

  • Enhancement: Reorganize examples

  • Bug Fix: get_pixel not working on MacOS Issue 539

Version 2.2.6

Release Date: Jan-20-2020

  • Bug Fix: particle_fireworks example is not running with 2.2.5 Issue 555

  • Bug Fix: Sprite.pop isn’t reliable Issue 531

  • Enhancement: Raise error if default font not found on system Issue 432

  • Enhancement: Add space invaders clone to example list Issue 526

  • Enhancement: Add sitemap to website

  • Enhancement: Improve performance, error handling around setting a sprite’s color

  • Enhancement: Implement optional filtering parameter to SpriteList.draw Issue 405

  • Enhancement: Return list of items hit during physics engine update Issue 401

  • Enhancement: Update resources documentation Issue 549

  • Enhancement: Add on_update to sprites, which includes delta_time Issue 266

  • Enhancement: Close enhancement-related github issues and reference them in the new Enhancement List.

Version 2.2.5

Release Date: Jan-17-2020

  • Enhancement: Improved speed when rendering non-buffered drawing primitives

  • Bug fix: Angle working in radians instead of degrees in 2.2.4 Issue 552

  • Bug fix: Angle and color of sprite not updating in 2.2.4 Issue 553

Version 2.2.4

Release Date: Jan-15-2020

  • Enhancement: Moving sprites now 20% more efficient.

Version 2.2.3

Release Date: Jan-12-2020

  • Bug fix: Hit boxes not getting updated with rotation and scaling. Issue 548 This update depricates Sprite.points and instead uses Sprint.hit_box and Sprint.get_adjusted_hit_box

  • Major internal change around not having __init__ do import * but specifically name everything. Issue 537 This rearranded a lot of files and also reworked the quickindex in documentation.

Version 2.2.2

Release Date: Jan-09-2020

  • Bug fix: Arcade assumes tiles in tileset are same sized Issue 550

Version 2.2.1

Release Date: Dec-22-2019

  • Bug fix: Resource folder not included in distribution Issue 541

Version 2.2.0

Release Date: Dec-19-2019*

  • Major Enhancement: Add built-in resources support Issue 209 This also required many changes to the code samples, but they can be run now without downloading separate images.

  • Major Enhancement: Auto-calculate hit box points by trimming out the transparency

  • Major Enhancement: Sprite sheet support for the tiled map editor works now

  • Enhancement: Added load_spritesheet for loading images from a sprite sheet

  • Enhancement: Updates to physics engine to better handle non-rectangular sprites

  • Enhancement: Add SpriteSolidColor class, for creating a single-color rectangular sprite

  • Enhancement: Expose type hints to modules that depend on arcade via PEP 561 Issue 533 and Issue 534

  • Enhancement: Add font_color to gui.TextButton init Issue 521

  • Enhancement: Improve error messages around loading tilemaps

  • Bug fix: Turn on vsync as it sometimes was limiting FPS to 30.

  • Bug fix: get_tile_by_gid() incorrectly assumes tile GID cannot exceed tileset length Issue 527

  • Bug fix: Tiles in object layers not placed properly Issue 536

  • Bug fix: Typo when loading font Issue 518

  • Updated requirements.txt file

  • Add robots.txt to documentation

Please also update pyglet, pyglet_ffmpeg2, and pytiled_parser libraries.

Special tanks to Jon Fincher, Mr. Gallo, SirGnip, lubie0kasztanki, and EvgeniyKrysanoc for their contributions to this release.

Version 2.1.7

  • Enhancement: Tile set support. Issue 511

  • Bug fix, search file tile images relative to tile map. Issue 480

Version 2.1.6

  • Fix: Lots of fixes around positioning and hitboxes with tile maps Issue 503

  • Documentation updates, particularly using on_update instead of update and remove_from_sprite_lists instead of kill. Issue 381

  • Remove/adjust some examples using csvs for maps

Version 2.1.5

  • Fix: Default font sometimes not pulling on mac Issue 488

  • Documentation updates, particularly around examples for animated characters on platformers

  • Fix to Sprite class to better support character animation around ladders

Version 2.1.4

  • Fix: Error when importing arcade on Raspberry Pi 4 Issue 485

  • Fix: Transparency not working in draw functions Issue 489

  • Fix: Order of parameters in draw_ellipse documentation Issue 490

  • Raise better error on data classes missing

  • Lots of code cleanup from SirGnip Issue 484

  • New code for buttons and dialog boxes from wamiqurrehman093 Issue 476

Version 2.1.3

  • Fix: Ellipses drawn to incorrect dimensions Issue 479

  • Enhancement: Add unit test for debugging Issue 478

  • Enhancement: Add more descriptive error when file not found Issue 472

  • Enhancement: Explicitly state delta time is in seconds Issue 473

  • Fix: Add missing ‘draw’ function to view Issue 470

Version 2.1.2

  • Fix: Linked to wrong version of Pyglet Issue 467

Version 2.1.1

  • Added pytiled-parser as a dependency in setup.py

Version 2.1.0

Thanks to SirGnip, Mr. Gallow, and Christian Clauss for their contributions.

Version 2.0.9

  • Fix: Unable to specify path to .tsx file for tiled spritesheet Issue 360

  • Fix: TypeError: __init__() takes from 3 to 11 positional arguments but 12 were given in text.py Issue 373

  • Fix: Test create_line_strip Issue 379

  • Fix: TypeError: draw_rectangle_filled() got an unexpected keyword argument ‘border_width’ Issue 385

  • Fix: See about creating a localization/internationalization example Issue 391

  • Fix: Glitch when you die in the lava in 09_endgame.py Issue 392

  • Fix: No default font found on ArchLinux and no error message (includes patch) Issue 402

  • Fix: Update docs around batch drawing and array_backed_grid.py example Issue 403

Version 2.0.8

  • Add example code from lixingque

  • Fix: Drawing primitives example broke in prior release Issue 365

  • Update: Improve automated testing of all code examples Issue 326

  • Update: raspberry pi instructions, although it still doesn’t work yet

  • Fix: Some buffered draw commands not working Issue 368

  • Remove yml files for build environments that don’t work because of OpenGL

  • Update requirement.txt files

  • Fix mountain examples

  • Better error handling when playing sounds

  • Remove a few unused example code files

Version 2.0.7

  • Last release improperly required pyglet-ffmpeg, updated to pyglet-ffmpeg2

  • Fix: The alpha value seems NOT work with draw_texture_rectangle Issue 364

  • Fix: draw_xywh_rectangle_textured error Issue 363

Version 2.0.6

  • Improve ffmpeg support. Think it works on MacOS and Windows now. Issue 350

  • Improve buffered drawing command support

  • Improve PEP-8 compliance

  • Fix for tiled map reader, Issue 360

  • Fix for animated sprites Issue 359

  • Remove unused avbin library for mac

Version 2.0.5

  • Issue if scale is set for a sprite that doesn’t yet have a texture set. Issue 354

  • Fix for Sprite.set_position not working. Issue 356

Version 2.0.4

  • Fix for drawing with a border width of 1 Issue 352

Version 2.0.3

Version 2.0.2 was compiled off the wrong branch, so it had a bunch of untested code. 2.0.3 is what 2.0.2 was supposed to be.

Version 2.0.2

  • Fix for loading a wav file Issue 344

  • Fix Linux only getting 30 fps Issue 342

  • Fix error on window creation Issue 340

  • Fix for graphics cards not supporting multi-sample Issue 339

  • Fix for set view error on mac Issue 336

  • Changing scale attribute on Sprite now dynamically changes sprite scale Issue 331

Version 2.0.1

  • Turn on multi-sampling so lines could be anti-aliased Issue 325

Version 2.0.0

Released 2019-03-10

Lots of improvements in 2.0.0. Too many to list, but the two main improvements:

  • Using shaders for sprites, making drawing sprites incredibly fast.

  • Using ffmpeg for sound.

Version 1.3.7

Released 2018-10-28

  • Fix for Issue 275 where sprites can get blurry.

Version 1.3.6

Released 2018-10-10

  • Bux fix for spatial hashing

  • Implement commands for getting a pixel, and image from screen

Version 1.3.5

Released 08-23-2018

Bug fixes for spatial hashing and sound.

Version 1.3.4

Released 28-May-2018

New Features
  • Issue 197: Add new set of color names that match CSS color names

  • Issue 203: Add on_update as alternative to update

  • Add ability to read .tmx files.

Bug Fixes
  • Issue 159: Fix array backed grid buffer example

  • Issue 177: Kind of fix issue with gi sound library

  • Issue 180: Fix up API docs with sound

  • Issue 198: Add start of isometric tile support

  • Issue 210: Fix bug in MacOS sound handling

  • Issue 213: Update code with gi streamer

  • Issue 214: Fix issue with missing images in animated sprites

  • Issue 216: Fix bug with venv

  • Issue 222: Fix get_window when using a Window class

Documentation
  • Issue 217: Fix typo in doc string

  • Issue 198: Add example showing start of isometric tile support

Version 1.3.3

Released 2018-May-05

New Features
  • Issue 184: For sound, wav, mp3, and ogg should work on Linux and Windows. wav and mp3 should work on Mac.

Updated Examples
  • Add happy face drawing example

Version 1.3.2

Released 2018-Apr-20

New Features
  • Issue 189: Add spatial hashing for faster collision detection

  • Issue 191: Add function to get the distance between two sprites

  • Issue 192: Add function to get closest sprite in a list to another sprite

  • Issue 193: Improve decorator support

Updated Documentation
  • Link the class methods in the quick index to class method documentation

  • Add mountain midpoint displacement example

  • Improve CSS

  • Add “Two Worlds” example game

Updated Examples
  • Update sprite_collect_coints_move_down.py to not use all_sprites_list

  • Update sprite_bullets_aimed.py to add a warning about how to manage text on a scrolling screen

  • Issue 194: Fix for calculating distance traveled in scrolling examples

Version 1.3.1

Released 2018-Mar-31

New Features
  • Update create_rectangle code so that it uses color buffers to improve performance

  • Issue 185: Add support for repeating textures

  • Issue 186: Add support for repeating textures on Sprites

  • Issue 184: Improve sound support

  • Issue 180: Improve sound support

  • Work on improving sound support

Updated Documentation
  • Update quick-links on homepage of http://arcade.academy

  • Update Sprite class documentation

  • Update copyright date to 2018

Updated Examples
  • Update PyMunk example code to use keyboard constants rather than hard-coded values

  • New sample code showing how to avoid placing coins on walls when randomly placing them

  • Improve listing/organization of sample code

  • Work at improving sample code, specifically try to avoid using all_sprites_list

  • Add PyMunk platformer sample code

  • Unsuccessful work at getting TravisCI builds to work

  • Add new sample for using shape lists

  • Create sample code showing difference in speed when using ShapeLists.

  • Issue 182: Use explicit imports in sample PyMunk code

  • Improve sample code for using a graphic background

  • Improve collect coins example

  • New sample code for creating caves using cellular automata

  • New sample code for creating caves using Binary Space Partitioning

  • New sample code for explosions

Version 1.3.0

Released 2018-February-11.

Enhancements

Version 1.2.5

Released 2017-December-29.

Bug Fixes
Enhancements

Version 1.2.4

Released 2017-December-23.

Bug Fixes

Version 1.2.3

Released 2017-December-20.

Bug Fixes
  • Issue 44: Improve wildcard imports

  • Issue 150: “Shapes” example refers to chapter that does not exist

  • Issue 157: Different levels example documentation hook is messed up.

  • Issue 160: sprite_collect_coins example fails to run

  • Issue 163: Some examples aren’t loading images

Enhancements
  • Issue 84: Allow quick running via -m

  • Issue 149: Need better error message with check_for_collision

  • Issue 151: Need example showing how to go between rooms

  • Issue 152: Standardize name of main class in examples

  • Issue 154: Improve GitHub compatibility

  • Issue 155: Improve readme documentation

  • Issue 156: Clean up root folder

  • Issue 162: Add documentation with performance tips

  • Issue 164: Create option for a static sprite list where we don’t check to see if things moved.

  • Issue 165: Improve error message with physics engine

Version 1.2.2

Released 2017-December-02.

Bug Fixes
  • Issue 143: Error thrown when using scroll wheel

  • Issue 128: Fix infinite loop in physics engine

  • Issue 127: Fix bug around warning with Python 3.6 when imported

  • Issue 125: Fix bug when creating window on Linux

Enhancements
  • Issue 147: Fix bug building documentation where two image files where specified incorrectly

  • Issue 146: Add release notes to documentation

  • Issue 144: Add code to get window and viewport dimensions

  • Issue 139: Add documentation on what collision_radius is

  • Issue 131: Add example code on how to do full-screen games

  • Issue 113: Add example code showing enemy turning around when hitting a wall

  • Issue 67: Improved support and documentation for joystick/game controllers

How to Contribute

We would love to have you contribute to the project! There are several ways that you can do so.

How to contribute without coding

  • Community - Post your projects, code, screen-shots, and discuss the Arcade library on the Python Arcade SubReddit.

  • Try coding your own animations and games. Write down notes on anything that is difficult to implement or understand about the library.

  • Suggest improvements - Post bugs and enhancement requests at the Github Issue List.

How to contribute code

First, take some time to understand the project layout:

Then you can improve these parts of the project:

  • Document - Edit the reStructuredText and docstrings to make the Arcade documentation better.

  • Test - Improve the unit testing.

  • Code - Contribute bug fixes and enhancements to the code.

A list of enhancements people have requested is available on the Enhancement List page.

Arcade Performance Information

Drawing primitive performance: Drawing lines, rectangles, and circles can be slow. If you are encountering this, you can speed things up by using ShapeElement lists where you batch together the drawing commands.

Sprite drawing performance: If your sprites don’t move, when creating the sprite list set is_static to True. This allows Arcade to load the sprites to the graphics card and just keep them there.

Collision detection performance: If it is taking too long to detect collisions between sprites, make sure that the SpriteList with all the sprites has use_spatial_hasing turned on.

Directory Structure

Directory

Description

\arcade

Source code for the arcade library.

\arcade\color

Submodule with predefined colors.

\arcade\key

Submodule with keyboard id codes.

\build

All built code from the compile script goes here.

\dist

Distributable Python wheels go here after the build script has run.

\doc

Arcade documentation. Note that API documentation is in docstrings along with the source.

\doc\source

Source rst files for the documentation.

\doc\source\examples

All the example code

\doc\source\images

Images used in the documentation.

\doc\build\html

After making the documentation, all the HTML code goes here. Look at this in a web browser to see what the documentation will look like.

\examples

Example code showing how to use Arcade.

\tests

Unit tests. Most unit tests are part of the docstrings.

Also see How to Compile.

How to Compile

Windows

Prep your system by getting the needed Python packages, listed in the requirements.txt file.

Create your own fork of the repository, and then clone it on your computer.

From the base directory, there is a “make” batch file that can be run with a number of different arguments, some of them listed here:

  • make full - This compiles the source, installs the package on the local computer, compiles the documentation, and runs all the unit tests.

  • make doc - This compiles the documentation. Resulting documentation will be in doc/build/html.

  • make fast - This compiles and installs the source code. It does not create the documentation or run any unit tests.

  • make test - This runs the tests.

  • make - Displays all the arguments “make” supports.

Note: Placing test programs in the root of the project folder will pull from the source code in the arcade library, rather than the library installed in the Python interpreter. This is helpful because you can avoid the compile step. Just make sure not to check in your test code.

Linux

Create your own fork of the repository, and then clone it on your computer.

Prep your system by downloading the needed packages:

sudo apt-get install python-dev

How to Submit Changes

First, you should open up an issue or enhancement request on the Github Issue List.

Next, create your own fork of the Arcade library. The Arcade library is at:

https://github.com/pvcraven/arcade

Follow the How to Compile on how to build the code.

You can submit changes with a “pull request.” With a pull request you ask that another repository (in this case the Arcade library) “pull” your changes into the main code base.

If you aren’t familiar with how to do pull requests, the Stack Overflow discussion on pull requests is good.

There are two Continuous Integration machines building changes to the code base. Windows builds are done on AppVeyor. Linux build testing is done on TravisCI.

Enhancement List

This is a list of possible enhancements opened in GitHub, but not being actively worked on. These are all good ideas. If you are thinking of helping the Arcade library by working on one of these, please re-open the issue.

Drawing

  • Issue 283 Add ability to draw overlapping polygons with same transparency.

  • Issue 421 Add support for drawing rounded rectangles.

  • Issue 433 Add support for bitmapped fonts.

  • Issue 595 Optimize drawing functions by caching them

  • After updating to Pyglet 2.x, update text drawing to use Pyglet’s text drawing rather than the Pillow text drawing.

  • Explore using frame buffers to overlay non-moving text and icons on a scrolling screen.

  • Add support for Pyglets ImageMouseCursors

GUI

  • Explore using frame buffers to create windows

Sprites

  • Issue 291 Load a spritesheet with a texture_region map.

  • Issue 377 Support basic sprite animations like rotate, flicker, disappear, etc.

  • Issue 380 Add ability to specify a point the sprite rotates around.

  • Issue 419 Create function to get sprites at a particular point.

  • Issue 498 Add lighting effects.

  • Add bloom effects.

  • Issue 523 Add sprite trigger/example for onenter / onexit.

  • Issue 289 Be able to get Sprite position and velocity as vectors.

  • Be able to load an animated gif as an animated time-based sprite.

  • Be able to load an Aesprite image directly (Piggy-back of Pyglet support)

Physics Engine

  • Issue 499 Create PyMunk + TMX example.

  • Issue 500 Show ‘rope’ effect.

  • Issue 524 Add example for “push back”.

  • Create a simplified front-end to the PyMunk physics engine

Event Processing

Tilemaps

  • Issue 375 Support rotated tiles in .tmx.

  • Issue 478 Processing object layer needs better support.

Documentation

  • Issue 452 Documentation Request - explain how delta_time works to help learners fully understand both how and why.

Examples

  • Issue 345 Create example showing how to manage multiple windows.

  • Issue 371 Create example showing how to bounce items off walls.

  • Issue 397 Add example code showing how to do parallax.

  • Issue 446 Add more procedural generation examples.

  • Issue 464 Add example for checkers-like game.

  • Add example showing how to use lerp or smoothstep for smooth camera panning.

Testing

  • Issue 450 Create unit tests for new View class.

This enhancement is not currently in process. Please re-open if you’d like to work on it. A full list of desired enhancements is available at:

http://arcade.academy/enhancement_list.html

Edge Artifacts

When working with images, particularly ones with transparency, graphics cards can create graphic artifacts on their edges. Images can have ‘borders’ where they aren’t wanted. For example, here there’s a line on the top and left:

_images/p0.png

Why does this happen? How do we fix it?

Why Edge Artifacts Appear

This happens when the edge of an image does not fall cleanly onto an image.

Edge Mis-Alignment

Typically edge artifacts happen when the edge of an image doesn’t land on an exact pixel boundary. Below in Figure 1, the left image is 128 pixels square and drawn at (100, 100), and looks fine. The image on the right is drawn with a center of (100, 300.5) and has an artifact that shows up as a line on the left edge. That artifact will not appear if the sprite is drawn at (100, 300) instead of (100, 300.5)

_images/p1.png

Figure 1: Edge artifacts caused by images that aren’t on integer pixel boundaries.

The left edge falls on a coordinate of 300.5 - (128/2) = 236.5. The computer tries to select a color that’s an average between 236 and 237, but since there is no 237 we get a dark color. Typically this only happens if the edge is transparent.

A shape that has a height or width that is not evenly divisible by two can also cause artifacts. If the shape is 15 pixels wide, then the center will fall between the 7th and 8th pixel making it harder to line up the pixels to the screen.

Scaling

Scaling an image can also cause artifacts. In Figure 2, the second sprite is scaled down by two-thirds. Since 128 pixels doesn’t evenly scale down by two-thirds, we end up with edge artifacts. If we had scaled down by one-half, that is possible to do with 128 pixels (to 64), so there would be no artifacts.

The third image in Figure 2 is scaled up by a factor of two. The edge spans two pixels and we end up with a line artifact as well. (Scaling down by two usually works if the image is divisible by four. Scaling up typically doesn’t.)

_images/p2.png

Figure 2: Edge artifacts caused by scaling.

Rotating

With rotation, it can be very difficult to get pixels lined up, and edge artifacts are common.

Improper Viewport

If a window is 800 wide, and the viewport is set to 799 or 801, then lines can also appear. Alternatively, if a viewport left or right edge is set to a non-integer number such as 23.5, this can cause the artifacts to appear.

_images/p3.png

Figure 3: Incorrect viewport

Solutions

Keeping sprite sizes to a power of two will help. For pixel-art types of games, using the GL_NEAREST filter can also help.

Use The Power of Two

Keep all sprite dimensions a power of two. Such as 2, 4, 8, 16, 32, 64, 128, 256, etc. It is ok if they aren’t square, such as a 32x64 pixel is fine.

Don’t scale up, only scale down. Also, only scale down in powers of two.

Aligning to the Nearest Pixel

By default, Arcade draws sprites with a filter called “linear” which makes for smoother scaling and lines. If instead you want a pixel-look, you can use a different filter called “nearest.” This filter also reduces issues with edge artifacts. First, import the filters at the top of your program:

from pyglet.gl import GL_NEAREST
from pyglet.gl import GL_LINEAR

Then, in your on_draw update the drawing of your sprites with the filter:

def on_draw(self):
    self.my_sprite_list.draw(filter=GL_NEAREST)
Double-Check Viewport Code

Double-check your viewport code to make sure the edges are only set to integers and the size of the window matches up exactly, without any off-by-one errors.

Logging

Arcade has a few options to log additional information around timings and how things are working internally. The two major ways to do this by turning on logging, and by querying the OpenGL context.

Turn on logging

The quickest way to turn on logging is to add this to the start of your main program file:

arcade.configure_logging()

This will cause the Arcade library to output some basic debugging information:

2409.0003967285156 arcade.sprite_list DEBUG - [386411600] Creating SpriteList use_spatial_hash=True is_static=False
2413.9978885650635 arcade.gl.context INFO - Arcade version : 2.4a5
2413.9978885650635 arcade.gl.context INFO - OpenGL version : 3.3
2413.9978885650635 arcade.gl.context INFO - Vendor         : NVIDIA Corporation
2413.9978885650635 arcade.gl.context INFO - Renderer       : GeForce GTX 980 Ti/PCIe/SSE2
2413.9978885650635 arcade.gl.context INFO - Python         : 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 19:29:22) [MSC v.1916 32 bit (Intel)]
2413.9978885650635 arcade.gl.context INFO - Platform       : win32
3193.9964294433594 arcade.sprite_list DEBUG - [386411600] _calculate_sprite_buffer: 0.013532099999999936 sec
Custom Log Configurations

If you want to add your own logging, or change the information printed in the log, you can do it with just a bit more code.

First, in your program import the logging library:

import logging

The code to turn on logging looks like this:

logging.basicConfig(level=logging.DEBUG)

You can get even more information by using a formatter to add time, file name, and even line number information to your output:

format = '%(asctime)s,%(msecs)03d %(levelname)-8s [%(filename)s:%(lineno)d %(funcName)s()] %(message)s'
logging.basicConfig(format=format,
                    datefmt='%H:%M:%S',
                    level=logging.DEBUG)

…which changes the output to look like:

13:40:50,226 DEBUG    [sprite_list.py:720 _calculate_sprite_buffer()] [365177904] _calculate_sprite_buffer: 0.00849660000000041 sec
13:40:50,398 DEBUG    [ui_element.py:58 on_mouse_over()] UIElement mouse over

You can add logging to your own programs by putting one of these lines at the top of your program:

# Get your own logger
LOG = logging.getLogger(__name__)
# or get Arcade's logger
LOG = logging.getLogger('arcade')

Then, any time you want to print, just use:

LOG.debug("This is my debug statement.")

Getting OpenGL Stats Using Query Objects

If you’d like more information on the time it takes to draw, you can query the OpenGL context arcade.Window.ctx as this example shows:

def on_draw(self):
    """ Render the screen. """
    arcade.start_render()

    query = self.ctx.query()
    with query:
        # Put the drawing commands you want to get info on here:
        self.my_sprite_list.draw()

    print()
    print(f"Time elapsed       : {query.time_elapsed:,} ns")
    print(f"Samples passed     : {query.samples_passed:,}")
    print(f"Primitives created : {query.primitives_generated:,}")

The output from this looks like the following:

Time elapsed       : 7,136 ns
Samples passed     : 390,142
Primitives created : 232