让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 18:23
哇哦,小陈的朋友吗
可以算吧
页:
[1]