add controller support

This commit is contained in:
csd4ni3l
2025-06-25 11:40:26 +02:00
parent 7d0d25ddb5
commit 136d982395
8 changed files with 104 additions and 33 deletions

View File

@@ -1,6 +1,12 @@
The cursor used for virtual controller support is from the Pixel UI pack
by Kenney Vleugels for Kenney (www.kenney.nl)
with help by Lynn Evers (Twitter: @EversLynn)
Licensed (Creative Commons Zero, CC0) https://creativecommons.org/publicdomain/zero/1.0/
Thanks to Pixabay for the sound effects and the music! Thanks to Pixabay for the sound effects and the music!
The Roboto Black font used in this project is licensed under the Open Font License. Read assets/fonts/OFL.txt for more information. The Roboto Black font used in this project is licensed under the Open Font License.
Read assets/fonts/OFL.txt for more information.
Huge Thanks to Python for being the programming language used in this game. Huge Thanks to Python for being the programming language used in this game.
https://www.python.org/ https://www.python.org/

BIN
assets/graphics/cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

View File

@@ -1,10 +1,11 @@
import arcade, arcade.gui, os, time import arcade, arcade.gui, os, time
from game.file_support import save_file from game.file_support import save_file
from utils.constants import button_style, dropdown_style from utils.constants import button_style
from utils.preload import button_texture, button_hovered_texture from utils.preload import button_texture, button_hovered_texture
from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
from arcade.gui.experimental.focus import UIFocusGroup
class FileManager(arcade.gui.UIView): class FileManager(arcade.gui.UIView):
def __init__(self, start_directory, allowed_extensions, save=False, *args): def __init__(self, start_directory, allowed_extensions, save=False, *args):
@@ -17,7 +18,7 @@ class FileManager(arcade.gui.UIView):
self.args = args self.args = args
self.save = save self.save = save
self.anchor = self.ui.add(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) self.anchor = self.add_widget(UIFocusGroup(size_hint=(1, 1)))
self.box = self.anchor.add(arcade.gui.UIBoxLayout(size_hint=(0.7, 0.7)), anchor_x="center", anchor_y="center") self.box = self.anchor.add(arcade.gui.UIBoxLayout(size_hint=(0.7, 0.7)), anchor_x="center", anchor_y="center")
self.content_cache = {} self.content_cache = {}
@@ -48,6 +49,8 @@ class FileManager(arcade.gui.UIView):
self.save_button = self.anchor.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Save', style=button_style, width=200, height=100), anchor_x="right", anchor_y="bottom", align_x=-35, align_y=5) self.save_button = self.anchor.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Save', style=button_style, width=200, height=100), anchor_x="right", anchor_y="bottom", align_x=-35, align_y=5)
self.save_button.on_click = lambda event: self.save_content() self.save_button.on_click = lambda event: self.save_content()
self.anchor.detect_focusable_widgets()
self.show_directory() self.show_directory()
def submit(self, content): def submit(self, content):
@@ -118,9 +121,23 @@ class FileManager(arcade.gui.UIView):
else: else:
self.file_buttons[-1].on_click = lambda event, file=f"{self.current_directory}/{file}": self.submit(file) self.file_buttons[-1].on_click = lambda event, file=f"{self.current_directory}/{file}": self.submit(file)
self.anchor.detect_focusable_widgets()
def on_mouse_press(self, x, y, button, modifiers):
if button == arcade.MOUSE_BUTTON_RIGHT:
self.change_directory(os.path.dirname(self.current_directory))
def on_button_press(self, controller, name):
if name == "b":
self.change_directory(os.path.dirname(self.current_directory))
elif name == "start":
self.main_exit()
def on_key_press(self, symbol: int, modifiers: int) -> bool | None: def on_key_press(self, symbol: int, modifiers: int) -> bool | None:
super().on_key_press(symbol, modifiers)
if symbol == arcade.key.ESCAPE: if symbol == arcade.key.ESCAPE:
self.main_exit()
def main_exit(self):
from game.play import Game from game.play import Game
self.window.show_view(Game(*self.args)) self.window.show_view(Game(*self.args))
@@ -131,7 +148,3 @@ class FileManager(arcade.gui.UIView):
self.current_directory = directory self.current_directory = directory
self.show_directory() self.show_directory()
def on_mouse_press(self, x, y, button, modifiers):
if button == arcade.MOUSE_BUTTON_RIGHT:
self.change_directory(os.path.dirname(self.current_directory))

View File

@@ -1,9 +1,10 @@
import arcade, arcade.gui, random, time, json, os import arcade, arcade.gui, random, time, json, os, numpy as np
from game.file_support import load_file
from utils.constants import COLS, ROWS, CELL_SIZE, SPACING, button_style from utils.constants import COLS, ROWS, CELL_SIZE, SPACING, button_style
from utils.preload import create_sound, destroy_sound, button_texture, button_hovered_texture from utils.preload import create_sound, destroy_sound, button_texture, button_hovered_texture, cursor_texture
from game.game_of_life import create_numpy_grid, update_generation from game.game_of_life import create_numpy_grid, update_generation
import numpy as np from game.file_support import load_file
class Game(arcade.gui.UIView): class Game(arcade.gui.UIView):
def __init__(self, pypresence_client=None, generation=None, running=False, cell_grid=None, gps=10, load_from=None): def __init__(self, pypresence_client=None, generation=None, running=False, cell_grid=None, gps=10, load_from=None):
@@ -23,6 +24,10 @@ class Game(arcade.gui.UIView):
self.last_generation_update = time.perf_counter() self.last_generation_update = time.perf_counter()
self.last_info_update = time.perf_counter() self.last_info_update = time.perf_counter()
self.has_controller = False
self.controller_a_press = False
self.controller_b_press = False
self.pypresence_client = pypresence_client self.pypresence_client = pypresence_client
self.spritelist = arcade.SpriteList() self.spritelist = arcade.SpriteList()
self.last_create_sound = time.perf_counter() self.last_create_sound = time.perf_counter()
@@ -37,6 +42,7 @@ class Game(arcade.gui.UIView):
def on_show_view(self): def on_show_view(self):
super().on_show_view() super().on_show_view()
self.setup_grid(load_existing=self.cell_grid is not None) self.setup_grid(load_existing=self.cell_grid is not None)
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
@@ -66,10 +72,44 @@ class Game(arcade.gui.UIView):
self.save_button.on_click = lambda event: self.save() self.save_button.on_click = lambda event: self.save()
self.anchor.add(self.save_button, anchor_x="right", anchor_y="bottom", align_x=-5, align_y=5) self.anchor.add(self.save_button, anchor_x="right", anchor_y="bottom", align_x=-5, align_y=5)
if self.window.get_controllers():
self.cursor_sprite = arcade.Sprite(cursor_texture)
self.spritelist.append(self.cursor_sprite)
self.has_controller = True
def main_exit(self): def main_exit(self):
from menus.main import Main from menus.main import Main
self.window.show_view(Main(self.pypresence_client)) self.window.show_view(Main(self.pypresence_client))
def on_trigger_motion(self, controller, name, value):
if not value >= 0.9:
return
if name == "lefttrigger":
self.load()
elif name == "righttrigger":
self.save()
def on_stick_motion(self, controller, name, value):
if name == "leftstick":
value *= 3
self.cursor_sprite.center_x += value.x
self.cursor_sprite.center_y += value.y
def on_button_press(self, controller, name):
if name == "a":
self.controller_a_press = True
elif name == "b":
self.controller_b_press = True
elif name == "start":
self.main_exit()
def on_button_release(self, controller, name):
if name == "a":
self.controller_a_press = False
elif name == "b":
self.controller_b_press = False
def setup_grid(self, load_existing=False, randomized=False): def setup_grid(self, load_existing=False, randomized=False):
self.spritelist.clear() self.spritelist.clear()
@@ -91,7 +131,7 @@ class Game(arcade.gui.UIView):
cell = arcade.SpriteSolidColor(CELL_SIZE, CELL_SIZE, center_x=self.start_x + col * (CELL_SIZE + SPACING), center_y=self.start_y + row * (CELL_SIZE + SPACING), color=arcade.color.WHITE) cell = arcade.SpriteSolidColor(CELL_SIZE, CELL_SIZE, center_x=self.start_x + col * (CELL_SIZE + SPACING), center_y=self.start_y + row * (CELL_SIZE + SPACING), color=arcade.color.WHITE)
if not bool(self.cell_grid[row, col]): if not bool(self.cell_grid[row, col]):
cell.visible = bool(self.cell_grid[row, col]) cell.visible = False
self.sprite_grid[row][col] = cell self.sprite_grid[row][col] = cell
self.spritelist.append(cell) self.spritelist.append(cell)
@@ -170,9 +210,11 @@ class Game(arcade.gui.UIView):
arcade.unschedule(self.update_generation) arcade.unschedule(self.update_generation)
arcade.schedule(self.update_generation, self.generation_time) arcade.schedule(self.update_generation, self.generation_time)
if self.window.mouse[arcade.MOUSE_BUTTON_LEFT]: # type: ignore if self.window.mouse[arcade.MOUSE_BUTTON_LEFT] or self.controller_a_press: # type: ignore
grid_col = int((self.window.mouse.data["x"] - self.start_x + (CELL_SIZE / 2)) // (CELL_SIZE + SPACING)) # type: ignore x = self.window.mouse.data["x"] if not self.controller_a_press else self.cursor_sprite.left
grid_row = int((self.window.mouse.data["y"] - self.start_y + (CELL_SIZE / 2)) // (CELL_SIZE + SPACING)) # type: ignore y = self.window.mouse.data["y"] if not self.controller_a_press else self.cursor_sprite.top
grid_col = int((x - self.start_x + (CELL_SIZE / 2)) // (CELL_SIZE + SPACING)) # type: ignore
grid_row = int((y - self.start_y + (CELL_SIZE / 2)) // (CELL_SIZE + SPACING)) # type: ignore
if grid_col < 0 or grid_row < 0 or grid_row >= ROWS or grid_col >= COLS: if grid_col < 0 or grid_row < 0 or grid_row >= ROWS or grid_col >= COLS:
return return
@@ -188,9 +230,11 @@ class Game(arcade.gui.UIView):
self.sprite_grid[grid_row][grid_col].visible = True self.sprite_grid[grid_row][grid_col].visible = True
self.cell_grid[grid_row, grid_col] = 1 self.cell_grid[grid_row, grid_col] = 1
elif self.window.mouse[arcade.MOUSE_BUTTON_RIGHT]: # type: ignore elif self.window.mouse[arcade.MOUSE_BUTTON_RIGHT] or self.controller_b_press: # type: ignore
grid_col = int((self.window.mouse.data["x"] - self.start_x + (CELL_SIZE / 2)) // (CELL_SIZE + SPACING)) # type: ignore x = self.window.mouse.data["x"] if not self.controller_b_press else self.cursor_sprite.left
grid_row = int((self.window.mouse.data["y"] - self.start_y + (CELL_SIZE / 2)) // (CELL_SIZE + SPACING)) # type: ignore y = self.window.mouse.data["y"] if not self.controller_b_press else self.cursor_sprite.top
grid_col = int((x - self.start_x + (CELL_SIZE / 2)) // (CELL_SIZE + SPACING)) # type: ignore
grid_row = int((y - self.start_y + (CELL_SIZE / 2)) // (CELL_SIZE + SPACING)) # type: ignore
if grid_col < 0 or grid_row < 0 or grid_row >= ROWS or grid_col >= COLS: if grid_col < 0 or grid_row < 0 or grid_row >= ROWS or grid_col >= COLS:
return return

View File

@@ -1,13 +1,15 @@
import arcade, arcade.gui, asyncio, pypresence, time, json, copy import arcade, arcade.gui, asyncio, pypresence, time, json, copy
from utils.preload import button_texture, button_hovered_texture from utils.preload import button_texture, button_hovered_texture
from utils.constants import button_style, discord_presence_id from utils.constants import button_style, discord_presence_id
from utils.utils import FakePyPresence from utils.utils import FakePyPresence
from arcade.gui.experimental.focus import UIFocusGroup
class Main(arcade.gui.UIView): class Main(arcade.gui.UIView):
def __init__(self, pypresence_client=None): def __init__(self, pypresence_client=None):
super().__init__() super().__init__()
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout()) self.anchor = self.add_widget(UIFocusGroup(size_hint=(1, 1)))
self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x='center', anchor_y='center') self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x='center', anchor_y='center')
self.pypresence_client = pypresence_client self.pypresence_client = pypresence_client
@@ -59,6 +61,8 @@ class Main(arcade.gui.UIView):
self.settings_button = self.box.add(arcade.gui.UITextureButton(text="Settings", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=button_style)) self.settings_button = self.box.add(arcade.gui.UITextureButton(text="Settings", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=button_style))
self.settings_button.on_click = lambda event: self.settings() self.settings_button.on_click = lambda event: self.settings()
self.anchor.detect_focusable_widgets()
def play(self): def play(self):
from game.play import Game from game.play import Game
self.window.show_view(Game(self.pypresence_client)) self.window.show_view(Game(self.pypresence_client))

View File

@@ -1,4 +1,4 @@
import copy, pypresence, json, os import copy, pypresence, json
import arcade, arcade.gui import arcade, arcade.gui
@@ -6,7 +6,6 @@ from utils.constants import button_style, dropdown_style, slider_style, settings
from utils.utils import FakePyPresence from utils.utils import FakePyPresence
from utils.preload import button_texture, button_hovered_texture, theme_sound from utils.preload import button_texture, button_hovered_texture, theme_sound
from arcade.gui import UIBoxLayout, UIAnchorLayout
from arcade.gui.experimental.focus import UIFocusGroup from arcade.gui.experimental.focus import UIFocusGroup
class Settings(arcade.gui.UIView): class Settings(arcade.gui.UIView):
@@ -30,16 +29,16 @@ class Settings(arcade.gui.UIView):
self.modified_settings = {} self.modified_settings = {}
def create_layouts(self): def create_layouts(self):
self.anchor = self.add_widget(UIAnchorLayout(size_hint=(1, 1))) self.anchor = self.add_widget(UIFocusGroup(size_hint=(1, 1)))
self.box = UIBoxLayout(space_between=50, align="center", vertical=False) self.box = arcade.gui.UIBoxLayout(space_between=50, align="center", vertical=False)
self.anchor.add(self.box, anchor_x="center", anchor_y="top", align_x=10, align_y=-75) self.anchor.add(self.box, anchor_x="center", anchor_y="top", align_x=10, align_y=-75)
self.top_box = UIBoxLayout(space_between=self.window.width / 160, vertical=False) self.top_box = arcade.gui.UIBoxLayout(space_between=self.window.width / 160, vertical=False)
self.anchor.add(self.top_box, anchor_x="left", anchor_y="top", align_x=10, align_y=-10) self.anchor.add(self.top_box, anchor_x="left", anchor_y="top", align_x=10, align_y=-10)
self.key_layout = self.box.add(UIBoxLayout(space_between=20, align='left')) self.key_layout = self.box.add(arcade.gui.UIBoxLayout(space_between=20, align='left'))
self.value_layout = self.box.add(UIBoxLayout(space_between=13, align='left')) self.value_layout = self.box.add(arcade.gui.UIBoxLayout(space_between=13, align='left'))
def on_show_view(self): def on_show_view(self):
super().on_show_view() super().on_show_view()
@@ -67,6 +66,8 @@ class Settings(arcade.gui.UIView):
self.top_box.add(category_button) self.top_box.add(category_button)
self.anchor.detect_focusable_widgets()
def display_category(self, category): def display_category(self, category):
if hasattr(self, 'apply_button'): if hasattr(self, 'apply_button'):
self.anchor.remove(self.apply_button) self.anchor.remove(self.apply_button)
@@ -135,6 +136,8 @@ class Settings(arcade.gui.UIView):
self.apply_button.on_click = lambda e: self.apply_settings() self.apply_button.on_click = lambda e: self.apply_settings()
self.anchor.add(self.apply_button, anchor_x="right", anchor_y="bottom", align_x=-10, align_y=10) self.anchor.add(self.apply_button, anchor_x="right", anchor_y="bottom", align_x=-10, align_y=10)
self.anchor.detect_focusable_widgets()
def apply_settings(self): def apply_settings(self):
for config_key, value in self.modified_settings.items(): for config_key, value in self.modified_settings.items():
self.settings_dict[config_key] = value self.settings_dict[config_key] = value
@@ -218,7 +221,6 @@ class Settings(arcade.gui.UIView):
file.write(json.dumps(self.settings_dict, indent=4)) file.write(json.dumps(self.settings_dict, indent=4))
def update(self, setting=None, button_state=None, setting_type="bool"): def update(self, setting=None, button_state=None, setting_type="bool"):
setting_dict = settings[self.current_category][setting]
config_key = settings[self.current_category][setting]["config_key"] config_key = settings[self.current_category][setting]["config_key"]
if setting_type == "option": if setting_type == "option":

7
run.py
View File

@@ -7,14 +7,15 @@ import logging, datetime, os, json, sys, arcade
from utils.utils import get_closest_resolution, print_debug_info, on_exception from utils.utils import get_closest_resolution, print_debug_info, on_exception
from utils.constants import log_dir, menu_background_color from utils.constants import log_dir, menu_background_color
from menus.main import Main from menus.main import Main
from utils.preload import theme_sound # type: ignore # needed for preload from utils.preload import theme_sound # needed for preload
from arcade.experimental.controller_window import ControllerWindow
sys.excepthook = on_exception sys.excepthook = on_exception
pyglet.resource.path.append(os.getcwd()) pyglet.resource.path.append(os.getcwd())
pyglet.font.add_directory('./assets/fonts') pyglet.font.add_directory('./assets/fonts')
#__builtins__.print = lambda *args, **kwargs: logging.debug(" ".join(map(str, args))) __builtins__.print = lambda *args, **kwargs: logging.debug(" ".join(map(str, args)))
if not log_dir in os.listdir(): if not log_dir in os.listdir():
os.makedirs(log_dir) os.makedirs(log_dir)
@@ -72,7 +73,7 @@ else:
if settings.get("music", True): if settings.get("music", True):
theme_sound.play(volume=settings.get("music_volume", 50) / 100, loop=True) theme_sound.play(volume=settings.get("music_volume", 50) / 100, loop=True)
window = arcade.Window(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style) window = ControllerWindow(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style)
if vsync: if vsync:
window.set_vsync(True) window.set_vsync(True)

View File

@@ -2,6 +2,7 @@ import arcade.gui, arcade
button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button.png")) button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button.png"))
button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button_hovered.png")) button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button_hovered.png"))
cursor_texture = arcade.load_texture("assets/graphics/cursor.png")
create_sound = arcade.Sound("assets/sound/create.mp3") create_sound = arcade.Sound("assets/sound/create.mp3")
destroy_sound = arcade.Sound("assets/sound/destroy.mp3") destroy_sound = arcade.Sound("assets/sound/destroy.mp3")