You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kivy使用ScreenManager多屏开发时游戏渲染刷新异常

问题根因

渲染异常、刷新不生效核心是3个代码错误:

  • on_start方法中手动实例化了一个孤立的SnakeGame对象,时钟刷新事件绑定在这个没有被添加到组件树的对象上,但KV文件加载时GameWindow已经自动创建了另一个SnakeGame实例渲染到屏幕——你刷新的根本不是用户能看到的那个游戏组件。
  • SnakeGame类将snake_body、snake_trail、direction定义为类属性而非实例属性,多实例场景下会出现状态串扰;同时所有坐标、画布尺寸直接取全局Window值,没有适配ScreenManager嵌套布局下组件自身的实际位置和大小,直接导致渲染错位。
  • KV中定义的方向按钮没有绑定控制逻辑,触摸事件判断没有做区域校验,嵌套布局下坐标计算偏移导致滑动控制失效。
修复步骤
  • 删除on_start中手动创建SnakeGame的冗余逻辑,将时钟刷新绑定到屏幕上实际渲染的游戏实例,通过Screen的on_enter/on_leave事件控制游戏启动/暂停,避免后台空跑。
  • 把类层面的状态变量全部移到__init__方法中改为实例属性,杜绝多实例状态串扰。
  • 所有画布绘制、坐标计算改用组件自身的self.width/self.height/self.pos属性,绑定尺寸变化事件自动重绘画布,适配布局分配的实际区域。
  • 补全方向按钮的控制逻辑,给触摸判断增加区域校验,避免坐标偏移。
修正后核心代码

main.py

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.widget import Widget
from kivy.graphics import Rectangle, Color, Line
from random import randrange
from kivy.clock import Clock


SIZE = 40
SPEED = 5


class SnakeBody(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.size = (SIZE, SIZE)


class Food(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.size = (SIZE, SIZE)

    def move(self, max_w, max_h):
        self.x = randrange(0, int(max_w - SIZE), SIZE)
        self.y = randrange(0, int(max_h - SIZE), SIZE)


class SnakeGame(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.direction = (SIZE, 0)
        self.snake_body = []
        self.snake_trail = []
        self.game_event = None
        self.bind(pos=self.update_canvas, size=self.update_canvas)

    def update_canvas(self, *args):
        self.canvas.before.clear()
        with self.canvas.before:
            Color(0, 0, 1)
            Rectangle(pos=self.pos, size=self.size)
            Color(0, 0, 0)
            Line(points=[self.x, self.y, self.x, self.top, self.right, self.top, self.right, self.y, self.x, self.y], width=2)

    def add_body(self):
        body = SnakeBody()
        if len(self.snake_body) == 0:
            body.pos = (randrange(0, int(self.width - SIZE), SIZE),
                        randrange(0, int(self.height - SIZE), SIZE))
        else:
            body.pos = self.snake_trail[-2]
        self.add_widget(body)
        self.snake_body.append(body)

    def new_game(self):
        for body_parts in self.snake_body:
            self.remove_widget(body_parts)
        self.snake_trail = []
        self.snake_body = []
        self.direction = (SIZE, 0)
        self.ids.food.move(self.width, self.height)
        self.add_body()

    def start_game(self):
        if self.game_event:
            self.game_event.cancel()
        self.new_game()
        self.game_event = Clock.schedule_interval(self.refresh, 1 / SPEED)

    def set_dir(self, dir_vec):
        opposite = (-self.direction[0], -self.direction[1])
        if dir_vec != opposite:
            self.direction = dir_vec

    def on_touch_up(self, touch):
        if not self.collide_point(*touch.pos):
            return
        mx = touch.x - touch.opos[0]
        my = touch.y - touch.opos[1]
        if abs(mx) > abs(my):
            self.set_dir((SIZE, 0) if mx > 0 else (-SIZE, 0))
        else:
            self.set_dir((0, SIZE) if my > 0 else (0, -SIZE))

    def refresh(self, dt):
        head = self.snake_body[0]
        head.pos[0] += self.direction[0]
        head.pos[1] += self.direction[1]

        new_pos = (head.pos[0], head.pos[1])
        self.snake_trail.append(new_pos)
        while len(self.snake_trail) > len(self.snake_body) + 1:
            del self.snake_trail[0]

        for i, body in enumerate(self.snake_body):
            if body is head:
                continue
            body.pos = self.snake_trail[i]

        if head.pos == self.ids.food.pos:
            self.ids.food.move(self.width, self.height)
            self.add_body()

        for body in self.snake_body:
            if body is not head and head.pos == body.pos:
                self.new_game()

        if not (self.x <= head.pos[0] < self.right) or \
           not (self.y <= head.pos[1] < self.top):
            self.new_game()


class WindowManager(ScreenManager):
    pass


class MainWindow(Screen):
    pass


class GameWindow(Screen):
    def on_enter(self, *args):
        self.ids.snake_game.start_game()
    def on_leave(self, *args):
        if self.ids.snake_game.game_event:
            self.ids.snake_game.game_event.cancel()


kv = Builder.load_file("assets/GameStyle.kv")


class SnakeApp(App):
    def build(self):
        return kv

if __name__ == "__main__":
    SnakeApp().run()

KV文件(assets/GameStyle.kv)

#:import Window kivy.core.window.Window

WindowManager:
    MainWindow:
    GameWindow:


<MainWindow>:
    name: "MainWindow"
    BoxLayout:
        orientation: "vertical"
        BoxLayout:
            FloatLayout:
                canvas:
                    Color:
                        rgb: .25, .25, .25
                    Rectangle:
                        pos: 0, 0
                        size: root.width, root.height / 2
                    Color:
                        rgb: 0, 0, 1
                    Rectangle:
                        pos: 0, root.height / 2
                        size: root.width, root.height / 2
                    Color:
                        rgb: 0, 0, 0
                    Line:
                        points: 0, root.height / 2, 0, root.height, root.width, root.height, root.width, root.height / 2, 0, root.height / 2
                        width: 2
                Label:
                    text: "Snake\n"
                    pos_hint: {"center_x": .5, "center_y": .5}
                    halign: "center"
                    font_name: "Calibri"
                    font_size: 60
                    color: 1, 0, 0, 1
                Label:
                    text: "The game"
                    pos_hint: {"center_x": .5, "center_y": .4}
                    halign: "center"
                    font_name: "Calibri"
                    font_size: 60
                    color: 0, 1, 0, 1
        AnchorLayout:
            Button:
                text: "Let's play"
                size_hint: .9, .9
                on_press:
                    root.manager.transition.direction = "left"
                    app.root.current = "GameWindow"

<SnakeBody>:
    canvas:
        Color:
            rgb: 1, 0, 0
        Rectangle:
            size: root.size
            pos: root.pos

<Food>:
    canvas:
        Color:
            rgb: 0, 1, 0
        Rectangle:
            size: root.size
            pos: root.pos

<SnakeGame>:
    Food:
        id: food

<GameWindow>:
    name: "GameWindow"
    BoxLayout:
        orientation: "vertical"
        BoxLayout:
            SnakeGame:
                id: snake_game
        GridLayout:
            cols: 3
            size_hint_y: 0.4
            Widget:
            Button:
                text: "UP"
                on_press: snake_game.set_dir((0, 40))
            Widget:
            Button:
                text: "LEFT"
                on_press: snake_game.set_dir((-40, 0))
            Button:
                text: "DOWN"
                on_press: snake_game.set_dir((0, -40))
            Button:
                text: "RIGHT"
                on_press: snake_game.set_dir((40, 0))
        AnchorLayout:
            anchor_x: "left"
            anchor_y: "bottom"
            size_hint_y: 0.1
            Button:
                text: "Back"
                size_hint: .25, 1
                on_press:
                    root.manager.transition.direction = "right"
                    app.root.current = "MainWindow"

修复后切屏无渲染错位,游戏刷新逻辑正常,滑动和按钮控制均可生效,切回主屏幕时游戏自动暂停不会后台空跑。

内容的提问来源于stack exchange,提问作者MindW1n

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 08:09:15