目次

PyO3でLÖVEもどきを作る (3)

作成日: 2023-08-16 (水)

第10回 (最終回) PyO3でLÖVEもどきを作る (3)

プロジェクト pyove

Cargo.toml

[package]
name = "pyove"
version = "0.1.0"
edition = "2021"

[dependencies]
sdl2 = "0.35"

[dependencies.pyo3]
version = "0.19.2"
features = ["auto-initialize", "macros"]

src/main.rs

pub mod graphics;
pub mod pyovemodule;
pub mod sdl2global;

use pyo3::prelude::*;
use pyo3::types::PyDict;
use sdl2::event::Event;
use sdl2global::*;
use std::time::{Duration, Instant};
use std::{env, path::Path, process::exit};

fn main() -> PyResult<()> {
    //
    // コマンドライン引数のパース
    //
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        // コマンドライン引数がただ1つだけ与えられていなければ終了する
        eprintln!("Usage: {} GAMEDIR", args[0]);
        exit(1);
    }

    let gamedir = Path::new(&args[1]);
    let main_file = gamedir.join("main.py");
    if !main_file.exists() {
        // main.pyが存在しなければ終了する
        eprintln!("Error: {} not exists.", main_file.to_str().unwrap());
        exit(2);
    }

    // main.pyの内容を読み込む
    let main_code = std::fs::read_to_string(main_file)?;

    // activate Python interpreter
    Python::with_gil(|py| {
        // ゲームディレクトリをインポートパスに追加する
        let sys = PyModule::import(py, "sys")?;
        sys.getattr("path")?
            .getattr("append")?
            .call1((gamedir.to_str().unwrap(),))?;

        // pyoveをモジュールとして登録
        let pyove = pyovemodule::create_pyove(py)?;
        let py_modules: &PyDict = sys.getattr("modules")?.downcast()?;
        py_modules.set_item("pyove", pyove)?;

        // main.pyを実行
        py.run(&main_code, None, None)?;

        // これら3つの関数はPython側のコードで置き換え可能
        let user_load = pyove.getattr("load")?;
        let user_update = pyove.getattr("update")?;
        let user_draw = pyove.getattr("draw")?;

        user_load.call0()?;

        // ゲームループの準備
        let mut event_pump = sdl_event_pump();
        let nanosecs_per_update = Duration::from_nanos(1_000_000_000 / 60);
        let clock = Instant::now();
        let mut last_update = clock.elapsed();

        // ゲームループ
        'gameloop: loop {
            let start = clock.elapsed();

            // SDLイベント処理
            for event in event_pump.poll_iter() {
                match event {
                    Event::Quit { .. } => break 'gameloop,
                    _ => {}
                }
            }

            // アップデート
            let delta_time = (start - last_update).as_secs_f32();
            user_update.call1((delta_time,))?;

            // 描画
            sdl_render_set_draw_color((0, 0, 0));
            sdl_render_clear();
            user_draw.call0()?;
            sdl_render_present();

            // スリープでフレームレートを同期を強制
            let passed = clock.elapsed() - start;
            if passed < nanosecs_per_update {
                std::thread::sleep(nanosecs_per_update - passed);
            }
            last_update = start;
        }

        Ok(())
    })
}

src/pyovemodule.rs

use crate::graphics;
use pyo3::prelude::*;

#[pyfunction]
fn load() {
    println!("default load");
}

#[pyfunction]
fn draw() {
    println!("default draw");
}

#[pyfunction]
fn update(dt: f32) {
    println!("default update {}", dt);
}

pub fn create_pyove(py: Python<'_>) -> PyResult<&PyModule> {
    let m = PyModule::new(py, "pyove")?;
    m.add_function(wrap_pyfunction!(load, m)?)?;
    m.add_function(wrap_pyfunction!(draw, m)?)?;
    m.add_function(wrap_pyfunction!(update, m)?)?;
    graphics::register_graphics_module(py, m)?;
    Ok(m)
}

src/graphics.rs

//
// このプロジェクトではset_colorとrectangle関数のみを提供する
//
use crate::sdl2global::*;
use pyo3::prelude::*;
use sdl2::rect::Rect;

#[pyfunction]
#[pyo3(name = "setColor")]
//#[pyo3(pass_module)]
fn set_color(r: f32, g: f32, b: f32) {
    // extract normalized color values
    let ri = (r * 255.0) as u8;
    let gi = (g * 255.0) as u8;
    let bi = (b * 255.0) as u8;
    sdl_render_set_draw_color((ri, gi, bi));
}

#[pyfunction]
fn rectangle(mode: &str, x: i32, y: i32, w: u32, h: u32) {
    let rect = Rect::new(x, y, w, h);
    if mode == "fill" {
        sdl_render_fill_rect(rect);
    } else if mode == "line" {
        sdl_render_draw_rect(rect);
    } else {
        eprintln!("Warning: unknown draw mode {}", mode);
    }
}

pub fn register_graphics_module(py: Python<'_>, parent_module: &PyModule) -> PyResult<()> {
    // see: https://pyo3.rs/v0.19.2/module#python-submodules
    // サブモジュールとしてgraphicsを登録する
    // graphicsに、setColorとrectangleを登録する
    let m = PyModule::new(py, "graphics")?;
    m.add_function(wrap_pyfunction!(set_color, m)?)?;
    m.add_function(wrap_pyfunction!(rectangle, m)?)?;
    parent_module.add_submodule(m)?;
    Ok(())
}

src/sdl2global.rs

use sdl2::{pixels::Color, rect::Rect, render::WindowCanvas, EventPump, Sdl};
use std::cell::RefCell;

thread_local! {
    static SDL_CONTEXT: RefCell<Sdl> = init_sdl().unwrap();
    static CANVAS: RefCell<WindowCanvas> = init_canvas().unwrap();
}

fn init_sdl() -> Result<RefCell<Sdl>, String> {
    let sdl_context = sdl2::init()?;
    Ok(RefCell::new(sdl_context))
}

fn init_canvas() -> Result<RefCell<WindowCanvas>, String> {
    let video_subsystem = SDL_CONTEXT.with(|sdl| sdl.borrow().video())?;

    let window = video_subsystem
        .window("PYÖVE Example", 800, 600)
        .position_centered()
        .opengl()
        .build()
        .map_err(|e| e.to_string())?;

    let canvas = window.into_canvas().build().map_err(|e| e.to_string())?;

    Ok(RefCell::new(canvas))
}

pub fn sdl_event_pump() -> EventPump {
    SDL_CONTEXT.with(|sdl| sdl.borrow().event_pump()).unwrap()
}

pub fn sdl_render_clear() {
    CANVAS.with(|canvas| canvas.borrow_mut().clear());
}

pub fn sdl_render_present() {
    CANVAS.with(|canvas| canvas.borrow_mut().present());
}

pub fn sdl_render_set_draw_color<C>(color: C)
where
    C: Into<Color>,
{
    CANVAS.with(|canvas| canvas.borrow_mut().set_draw_color(color));
}

pub fn sdl_render_draw_rect(rect: Rect) {
    CANVAS
        .with(|canvas| canvas.borrow_mut().draw_rect(rect))
        .unwrap();
}

pub fn sdl_render_fill_rect(rect: Rect) {
    CANVAS
        .with(|canvas| canvas.borrow_mut().fill_rect(rect))
        .unwrap();
}

example/main.py

import pyove
#import foo # test import availability

# Python requires global scoped variable definitions
x, y, w, h = 0, 0, 0, 0

def load():
    global x, y, w, h
    x, y, w, h = 20, 20, 60, 20

def update(dt):
    global w, h
    w += 1
    h += 1

def draw():
    pyove.graphics.setColor(0, 0.4, 0.4)
    pyove.graphics.rectangle("fill", x, y, w, h)

# overwrite default module functions
pyove.load = load
pyove.draw = draw
pyove.update = update

参考

オリジナルの動作は、こちらを参考にしてください。