streetart 发表于 2025-8-24 13:57:52

让Python游戏开发和Scratch一样简单——Scrawl引擎

本帖最后由 streetart 于 2025-8-24 13:59 编辑

经过将近2个月的完善,Scrawl v0.12.1终于和大家见面啦!Scrawl v0.12.x将是v1.1.0稳定版发布前的最后一个主版本,我们将在这一版对Scrawl进行优化,稳定功能。

Github链接:github.com/streetartist/scrawl
QQ交流群:1001578435
文档:cold-coil-43b.notion.site/Scrawl-228c40bc2a0e809585dce4119e52681c?pvs=74
知乎介绍文章:zhuanlan.zhihu.com/p/1931324296887792267

欢迎加入Scrawl库的开发或使用Scrawl库!

那么,什么是Scrawl呢?

简而言之,就是我们希望能够在Python中实现Scratch游戏编程的范式,让Scratch转Python游戏编程更简单。

我们实现了哪些简化功能

[*]让大家可以愉快地写while True
[*]实现事件系统
[*]实现了云变量
[*]甚至实现了简单的GUI功能(基于pygameGUI)感谢@cjjJasonchen

AI生成的介绍:
告别复杂,拥抱创造:用Python轻松打造您的下一个游戏大作!
您是否曾梦想创造属于自己的游戏,却被复杂的游戏引擎和陡峭的学习曲线劝退?您是否熟悉Scratch的积木式编程,并渴望在Python的强大世界中找到同样直观有趣的创作体验?

现在,我们向您隆重介绍一个全新的Python游戏框架——它将Scratch的简洁理念与Python的强大功能完美融合,让游戏开发变得前所未有的简单、快速和充满乐趣!

核心理念:像玩积木一样写代码
我们深知,从图形化编程到文本代码的跨越是许多学习者面临的巨大挑战。因此,我们的框架在设计上处处体现着对初学者的友好:

熟悉的概念,无缝过渡:拥有“角色(Sprite)”、“场景(Scene)”、“造型(Costume)”和“广播(Broadcast)”等核心概念,如果您用过Scratch,就能立刻上手。
装饰器驱动,事件清晰:忘掉冗长的事件循环判断吧!只需一个简单的装饰器,就能让函数响应键盘或鼠标事件。代码直观得就像在说:“当按下空格键时,执行这个动作!”
# 代码就是这么简单!
from pygame.constants import K_SPACE

# 仅供示例,实际使用要在继承Sprite的类中使用。
@on_key(K_SPACE, "pressed")
def jump(self):
    self.play_sound("jump_sound")
    self.velocity.y = -10不止于简单,更有强大功能
简洁的设计之下,是毫不妥协的强大功能。无论您是想快速实现原型,还是打造功能丰富的游戏,这个框架都能满足您的需求。

高级碰撞检测:内置矩形(rect)、圆形(circle)和像素完美(mask)三种碰撞模式。您可以根据性能和精度的需求自由切换,从简单的方块碰撞到不规则图形的精确检测,尽在掌握。
内置物理引擎:通过 PhysicsSprite 类,为您的游戏角色一键添加重力、摩擦力和弹性效果。创建平台跳跃游戏或物理弹球从未如此轻松。
丰富的多媒体支持:轻松加载和播放背景音乐、音效,甚至可以动态生成鼓点和音符。更有开箱即用的粒子系统(ParticleSystem),为您的游戏增添华丽的视觉特效。
真正的“杀手级”功能:云变量(Cloud Variables)
想象一下,您的单机游戏能即时拥有在线功能吗?现在可以了!

我们独创的 CloudVariablesClient 类,让您仅用几行代码就能实现:

在线排行榜:实时记录并显示全球玩家的最高分。
多人数据同步:创建简单的在线协作或对战游戏,同步玩家位置、分数等关键数据。
游戏状态云端保存:让玩家可以跨设备继续他们的游戏进度。
这个功能将为您的创意插上翅膀,轻松实现过去需要复杂后端知识才能完成的在线交互。

为谁而生?
编程初学者和学生:这是从Scratch等图形化编程过渡到Python的最佳桥梁。在熟悉的理念下学习“真实世界”的编程范式。
游戏开发爱好者:无需学习庞大的商业引擎,用您钟爱的Python语言快速将创意变为现实。
教育工作者:一套完美的教学工具,能够生动地向学生展示事件驱动、面向对象和异步编程等核心计算机科学概念。
原型设计师:在投入大型项目前,用它快速验证您的游戏玩法和核心机制。
立即开始您的创作之旅!
停止观望,立即行动!这个框架已经为您铺平了通往游戏世界的所有道路。从一个简单的想法开始,添加角色,编写交互,部署特效,甚至连接云端。您的下一个游戏杰作,只差一个 import 的距离。

释放您的想象力,用Python和我们一起,将创造的乐趣带回编程!

多说无益,给大家看两个成品项目,只需要如此通俗的代码,即可实现炫酷小游戏:
1. 奇幻幽林:
(基于:scratch.mit.edu/projects/239626199/editor/ Scratch项目修改而来)
效果视频:s3plus.meituan.net/opapisdk/op_ticket_885190757_1756014248118_qdqqd_6uuzxy.mp4
代码与资源文件:github.com/streetartist/scrawl_demo_witch
主程序源码:
from scrawl import *
import pygame
import time

# svg files from scratch.mit.edu/projects/239626199/editor/
# 游戏说明:
# 通过左右方向键控制女巫旋转;
# 通过空格键控制火球发射(点一次发射一个);
# 碰到敌人就Game Over;
# 按a键使用屏障,屏障显示3秒,10秒可用一次。


# 创建游戏实例
game = Game()


class Bat1(Sprite):

    def __init__(self):
      super().__init__()
      self.name = "Bat1"

      self.add_costume("costume1",
                         pygame.image.load("bat1-b.svg").convert_alpha())
      self.add_costume("costume2",
                         pygame.image.load("bat1-a.svg").convert_alpha())
      self.visible = False
      self.set_size(0.5)

    @as_clones
    def clones1(self):
      self.pos = pygame.Vector2(400, 300)
      self.face_random_direction()
      self.move(400)
      self.face_towards("Witch")
      self.visible = True

      while True:
            self.next_costume()
            yield 300

    @as_clones
    def clones2(self):
      while True:
            self.move(8) # 快速蝙蝠
            yield 200

    @as_main
    def main1(self):
      while True:
            yield 3000
            # 添加蝙蝠
            self.clone()

    @handle_sprite_collision("FireBall")
    @handle_sprite_collision("Wall")
    def die(self, other):
      self.delete_self()

    @handle_sprite_collision("Witch")
    def hit_witch(self, other):
      self.delete_self()

class Bat2(Sprite):

    def __init__(self):
      super().__init__()
      self.name = "Bat2"

      self.add_costume("costume1",
                         pygame.image.load("bat2-b.svg").convert_alpha())
      self.add_costume("costume2",
                         pygame.image.load("bat2-a.svg").convert_alpha())
      self.visible = False
      self.set_size(0.5)

    @as_clones
    def clones1(self):
      self.pos = pygame.Vector2(400, 300)
      self.face_random_direction()
      self.move(400)
      self.face_towards("Witch")
      self.visible = True

      while True:
            self.next_costume()
            yield 300

    @as_clones
    def clones2(self):
      while True:
            self.move(5)
            yield 200

    @as_main
    def main1(self):
      while True:
            yield 3000
            # 添加蝙蝠
            self.clone()

    @handle_sprite_collision("Witch")
    def hit_witch(self, other):
      self.delete_self()
      
    @handle_sprite_collision("FireBall")
    @handle_sprite_collision("Wall")
    def die(self, other):
      self.delete_self()


class FireBall(Sprite):

    def __init__(self):
      super().__init__()
      self.name = "FireBall"
      self.add_costume("costume1",
                         pygame.image.load("ball-a.svg").convert_alpha())
      self.visible = False
      self.set_size(0.2)

    @as_clones
    def clones1(self):
      self.visible = True

      while True:
            self.move(10)
            yield 100

    @handle_edge_collision()
    def finish(self):
      self.delete_self()

class Wall(Sprite):
    def __init__(self):
      super().__init__()
      self.name = "Wall"
      self.add_costume("costume1",
                         pygame.image.load("wall.png").convert_alpha())
      self.set_size(0.5)
      self.last_use = time.time() # 记录上一次使用
      self.visible = False

    @on_key(pygame.K_a, "pressed")
    def use_wall(self):
      if time.time() - self.last_use >= 10: # 最多10秒用一次屏障
            self.visible = True
            yield 3000 # 屏障显示3秒
            self.visible = False
            self.last_use = time.time()
            

class Gameover(Sprite):
    def __init__(self):
      super().__init__()
      self.add_costume("costume1",
                         pygame.image.load("gameover.png").convert_alpha())
      self.visible = False

    @handle_broadcast("gameover")
    def gameover(self):
      self.visible = True

class Witch(Sprite):

    def __init__(self):
      super().__init__()
      self.name = "Witch"

      self.add_costume("costume1",
                         pygame.image.load("witch.svg").convert_alpha())

      self.fireball = FireBall()
      self.set_size(0.7)

    @on_key(pygame.K_RIGHT, "held")
    def right_held(self):
      self.turn_right(2)

    @on_key(pygame.K_LEFT, "held")
    def left_held(self):
      self.turn_left(2)

    @on_key(pygame.K_SPACE, "pressed")
    def space_pressed(self):
      self.fireball.direction = self.direction
      self.clone(self.fireball)

    @handle_sprite_collision("Bat1")
    @handle_sprite_collision("Bat2")
    def die(self):
      self.broadcast("gameover")


# 定义场景
class MyScene(Scene):

    def __init__(self):
      super().__init__()

      bat1 = Bat1()
      self.add_sprite(bat1)

      bat2 = Bat2()
      self.add_sprite(bat2)

      witch = Witch()
      self.add_sprite(witch)

      wall = Wall()
      self.add_sprite(wall)

      gameover = Gameover()
      self.add_sprite(gameover)


# 运行游戏
game.set_scene(MyScene())
game.run(fps=60)

2. Scrawl版的Flappybird:
完整源码:github.com/streetartist/scrawl_demo_flappybird
视频:s3plus.meituan.net/opapisdk/op_ticket_885190757_1756014484001_qdqqd_5eh880.mp4
主程序:import pygame
from pygame import K_SPACE
import random
from scrawl import Game, Scene, PhysicsSprite, Sprite, on_key, as_main, on_mouse_event, handle_edge_collision, handle_sprite_collision, CloudVariablesClient
import time

# 游戏常量
SCREEN_WIDTH = 288
SCREEN_HEIGHT = 512
GRAVITY = 0.5
FLAP_FORCE = -7
PIPE_SPEED = 2.5
PIPE_GAP = 120
PIPE_FREQUENCY = 1500# 每1500ms生成一个新管道

client = CloudVariablesClient(project_id="0a668321-a7f0-4663-8d68-27c515142875")

class Bird(PhysicsSprite):
    def __init__(self):
      super().__init__()
      self.name = "小鸟"
      self.set_collision_type("mask")
      
      # 创建小鸟图像
      bird_width = 24
      bird_height = 17
      surface = pygame.Surface((bird_width, bird_height), pygame.SRCALPHA)
      pygame.draw.ellipse(surface, (255, 200, 0), (0, 0, bird_width, bird_height))# 鸟身体
      pygame.draw.circle(surface, (255, 100, 0), (bird_width - 9, 6), 3)# 鸟头
      pygame.draw.polygon(surface, (255, 0, 0), [
            (bird_width - 6, 6),
            (bird_width + 4, 6),
            (bird_width - 1, 8)
      ])# 鸟嘴
      pygame.draw.ellipse(surface, (0, 0, 0), (bird_width - 12, 4, 3, 3))# 眼睛
      
      self.add_costume("default", surface)
      self.pos.x = SCREEN_WIDTH // 3# 初始位置在屏幕1/3处
      self.pos.y = SCREEN_HEIGHT // 2
      
      # 设置物理属性
      self.set_gravity(0, GRAVITY)
      self.set_elasticity(0.2)
      self.set_friction(0.99)
      
      # 游戏状态
      self.game_over = False
      self.score = 0
   
    @on_key(K_SPACE, "pressed")
    @on_mouse_event(button=1, mode="pressed")
    def flap(self):
      """按下空格键或鼠标使小鸟上升"""
      if not self.game_over:
            self.velocity.y = FLAP_FORCE
            self.play_sound("flap")# 播放拍翅膀音效
   
    @handle_edge_collision("top")
    def hit_top(self):
      """碰到上边界"""
      if not self.game_over:
            self.velocity.y = 0
            self.pos.y = 10
   
    @handle_edge_collision("bottom")
    @handle_sprite_collision("管道")
    def hit_bottom(self):
      """碰到下边界或管道 - 游戏结束"""
      if not self.game_over:
            self.game.log_debug("!!!!!")
            self.game_over = True
            self.say("Ouch!", 3000)
            self.play_sound("hit")# 播放撞击音效
            
            # 从云变量中获取当前最高分,如果不存在则默认为0
            current_highest = client.get_variable("highest_score", 0)
            
            # 如果当前分数高于最高分,则更新云变量
            if self.score > current_highest:
                client.set_variable("highest_score", self.score)
   
    @as_main
    def bird_physics(self):
      """处理小鸟物理状态"""
      while True:
            # 旋转小鸟基于下落速度
            self.direction = max(-30, min(self.velocity.y * 2, 90))
            yield 0

class Pipe(Sprite):
    def __init__(self, x, gap_y):
      super().__init__()
      
      self.name = "管道"
      self.set_collision_type("mask")
      
      # 随机管道高度
      top_height = gap_y - PIPE_GAP // 2
      bottom_height = SCREEN_HEIGHT - (gap_y + PIPE_GAP // 2)
      
      # 管道宽度
      pipe_width = 52 * (SCREEN_WIDTH / 400)
      
      # 直接在一个surface上绘制管道
      surface = pygame.Surface((pipe_width, SCREEN_HEIGHT), pygame.SRCALPHA)
      
      # 绘制上管道(从顶部向下绘制)
      pygame.draw.rect(surface, (0, 180, 0), (0, 0, pipe_width, top_height))
      # 上管道顶部装饰
      pygame.draw.rect(surface, (0, 140, 0), (0, top_height - 15, pipe_width, 15))
      
      # 绘制下管道(从间隙底部开始绘制)
      bottom_pipe_y = gap_y + PIPE_GAP // 2
      pygame.draw.rect(surface, (0, 180, 0), (0, bottom_pipe_y, pipe_width, bottom_height))
      # 下管道顶部装饰
      pygame.draw.rect(surface, (0, 140, 0), (0, bottom_pipe_y, pipe_width, 15))
      
      self.add_costume("default", surface)
      self.collision_mask = pygame.mask.from_surface(self.image)
      self.pos.x = x
      self.pos.y = SCREEN_HEIGHT // 2
      
      # 管道属性
      self.passed = False# 小鸟是否已通过该管道
   
    @as_main
    def move_pipe(self):
      """移动管道"""
      while True:
            if not self.scene.bird.game_over:
                self.pos.x -= PIPE_SPEED
               
                # 检测小鸟是否通过管道
                if not self.passed and self.pos.x < self.scene.bird.pos.x:
                  self.passed = True
                  self.scene.bird.score += 1
                  self.play_sound("point")# 播放得分音效
               
                # 管道移出屏幕后删除
                if self.pos.x < -100:
                  self.delete_self()
            
            yield 0

class Ground(Sprite):
    def __init__(self):
      super().__init__()
      self.name = "地面"
      
      # 创建地面图像
      ground_height = 40
      surface = pygame.Surface((SCREEN_WIDTH, ground_height))
      surface.fill((222, 184, 135))# 浅棕色地面
      
      # 添加草地纹理
      pygame.draw.rect(surface, (0, 180, 0), (0, 0, SCREEN_WIDTH, 8))
      for i in range(0, SCREEN_WIDTH, 15):
            pygame.draw.line(surface, (0, 140, 0), (i, 8), (i+8, 8), 2)
      
      self.add_costume("default", surface)
      self.pos.x = SCREEN_WIDTH // 2
      self.pos.y = SCREEN_HEIGHT - ground_height // 2

class ScoreSprite(Sprite):
    def __init__(self, bird):
      super().__init__()
      self.name = "得分显示"
      self.bird = bird# 引用小鸟对象以获取分数
      
      self.font = pygame.font.SysFont(None, 28)
      
      # 初始分数显示
      self.update_score()
      
      # 设置精灵位置 (居中靠上)
      self.pos.x = SCREEN_WIDTH // 2
      self.pos.y = 40
   
    def update_score(self):
      """更新分数显示"""
      # 渲染分数文本
      score_text = f"Score: {self.bird.score}"
      text_surface = self.font.render(score_text, True, (255, 255, 255))
      
      # 添加阴影效果
      shadow_surface = self.font.render(score_text, True, (0, 0, 0))
      
      # 创建最终表面(稍大一点以容纳阴影)
      self.surface = pygame.Surface((text_surface.get_width() + 4, text_surface.get_height() + 4), pygame.SRCALPHA)
      
      # 绘制阴影(偏移2像素)
      self.surface.blit(shadow_surface, (2, 2))
      # 绘制主文本
      self.surface.blit(text_surface, (0, 0))
      
      # 设置精灵造型
      self.add_costume("default", self.surface)
   
    @as_main
    def update_loop(self):
      """持续更新分数显示"""
      last_score = -1# 初始值确保第一次会更新
      while True:
            # 当分数变化时更新显示
            if self.bird.score != last_score:
                self.update_score()
                last_score = self.bird.score
            yield 0
            
# 最高分显示精灵
class HighestScoreDisplay(Sprite):
    def __init__(self):
      super().__init__()
      self.name = "最高分显示"
      
      self.font = pygame.font.SysFont(None, 24) # 字体稍小
      
      self.highest_score = 0
      self.update_display()
      
      # 设置位置,在当前分数下方
      self.pos.x = SCREEN_WIDTH // 2
      self.pos.y = 70
   
    def update_display(self):
      """更新最高分显示"""
      self.highest_score = client.get_variable("highest_score", 0) # 从云端获取,默认为0
      
      score_text = f"Highest: {self.highest_score}"
      text_surface = self.font.render(score_text, True, (255, 255, 255))
      shadow_surface = self.font.render(score_text, True, (0, 0, 0))
      
      self.surface = pygame.Surface((text_surface.get_width() + 4, text_surface.get_height() + 4), pygame.SRCALPHA)
      self.surface.blit(shadow_surface, (2, 2))
      self.surface.blit(text_surface, (0, 0))
      
      self.add_costume("default", self.surface)
   
    @as_main
    def update_loop(self):
      """持续更新最高分显示"""
      last_highest_score = -1
      while True:
            current_highest = client.get_variable("highest_score", 0)
            if current_highest != last_highest_score:
                self.update_display()
                last_highest_score = current_highest
            # 每0.1秒更新一次最高分
            yield 100

class FlappyScene(Scene):
    def __init__(self):
      super().__init__()
      self.name = "Flappy Bird"
      self.background_color = (135, 206, 235)# 天蓝色背景
      
      # 添加云朵装饰
      for _ in range(4):
            cloud = Sprite()
            cloud_size = random.randint(20, 50)
            surface = pygame.Surface((cloud_size*2, cloud_size), pygame.SRCALPHA)
            pygame.draw.ellipse(surface, (255, 255, 255), (0, 0, cloud_size*2, cloud_size))
            cloud.add_costume("default", surface)
            cloud.pos.x = random.randint(0, SCREEN_WIDTH)
            cloud.pos.y = random.randint(40, 150)# 降低云朵高度
            self.add_sprite(cloud)
      
      # 添加游戏元素
      self.bird = Bird()
      self.add_sprite(self.bird)
      
      self.ground = Ground()
      self.add_sprite(self.ground)
      
      # 添加得分显示精灵
      self.score_display = ScoreSprite(self.bird)
      self.add_sprite(self.score_display)

      # 添加最高分显示精灵
      self.highest_score_display = HighestScoreDisplay()
      self.add_sprite(self.highest_score_display)
   
    @as_main
    def generate_pipes(self):
      # 加载音效
      self.game.load_sound("flap", "sounds/flap.ogg")
      self.game.load_sound("hit", "sounds/hit.ogg")
      self.game.load_sound("point", "sounds/point.ogg")
      
      """定时生成新管道"""
      while True:
            if not self.bird.game_over:
                # 随机管道位置 (确保间隙在屏幕内)
                gap_y = random.randint(120, SCREEN_HEIGHT - 120)
                new_pipe = Pipe(SCREEN_WIDTH + 30, gap_y)
                self.add_sprite(new_pipe)
            
            yield PIPE_FREQUENCY# 等待一段时间后生成新管道
            
time.sleep(2)

# 创建并运行游戏
game = Game(width=SCREEN_WIDTH, height=SCREEN_HEIGHT, title="Flappy Bird - Scrawl 复刻版", fullscreen=True)
game.set_scene(FlappyScene())
game.run(fps=60)

萧随风 发表于 2025-8-24 14:53:04

牛逼

某一个“天” 发表于 2025-8-24 18:23:21

哇哦,小陈的朋友吗

streetart 发表于 2025-8-24 20:36:12

某一个“天” 发表于 2025-8-24 18:23
哇哦,小陈的朋友吗

可以算吧
页: [1]
查看完整版本: 让Python游戏开发和Scratch一样简单——Scrawl引擎