如何让图像在 pyglet window 中移动?

How to make an image move through a pyglet window?

我正在尝试使用 pyglet 制作动画。所以首先我尝试了一个简单的动画,在一条直线上移动图像。理想情况下,我希望它从左到右弹跳。

这是我的代码:

import pyglet

def center_image(image):
    """Sets an image's anchor point to its center"""
    image.anchor_x = image.width // 2
    image.anchor_y = image.height // 2

# Make window.
window = pyglet.window.Window(width=640, height=480)

# Load image.
pyglet.resource.path = ['images']
pyglet.resource.reindex()
heart_img = pyglet.resource.image('red-heart.png')
center_image(heart_img)

# Make animation sprite.
heart_grid = pyglet.image.ImageGrid(heart_img, rows=1, columns=5)
heart_ani = pyglet.image.Animation.from_image_sequence(heart_grid, duration=0.1)
heart_sprite = pyglet.sprite.Sprite(heart_ani, x=100, y=300)
heart_sprite.update(scale=0.05)

@window.event
def on_draw():
    window.clear()
    heart_sprite.draw()

if __name__ == '__main__':
    pyglet.app.run()

这段代码产生这个:

怎样才能让整颗心都动起来window?

理想的心脏轨迹应该是这样的:

方框是框架,拱门是轨迹,O是精灵。所以心脏会跳动每个单词的第一个字母,然后跳动精灵。

我的解决方案使用两个固定 Sprite 之间的中点来确定移动 Sprite 应该向上还是向下。为此,我将所有字母制作成单独的 Sprites,每个字母一个 png。

希望这张图片能更好地解释下面的代码。

#!/usr/bin/env python

import pyglet

CURR_BOUNCE = 0
MIDPOINTS = []
ENDPOINTS = []

def calculate_midpoint(s1, s2):
    """ Calculate the midpoint between two sprites on the x axis. """
    return (s1.x + s2.x) // 2

def should_move_down():
    """ Decides if the Sprite is going up or down. """
    global CURR_BOUNCE
    # If the Sprite completed all bounces the app closes (not the best solution).
    if max(len(MIDPOINTS), len(ENDPOINTS), CURR_BOUNCE) == CURR_BOUNCE:
        raise SystemExit
    # Move down if the Sprite is between the midpoint and the end (landing) point.
    if MIDPOINTS[CURR_BOUNCE] <= heart_sprite.x <= ENDPOINTS[CURR_BOUNCE]:
        return True
    # If the Sprite has passed both the mid and end point then it can move on to the next bounce.
    if max(MIDPOINTS[CURR_BOUNCE], heart_sprite.x, ENDPOINTS[CURR_BOUNCE]) == heart_sprite.x:
        CURR_BOUNCE += 1
    # Default behaviour is to keep going up.
    return False

def update(dt):
    """ 
    Move Sprite by number of pixels in each tick. 
    The Sprite always moves to the right on the x-axis.
    The default movement on the y-axis is up.
    """
    heart_sprite.x += dt * heart_sprite.dx
    if should_move_down():
        # To go down simply make the movement on the y-axis negative.
        heart_sprite.y -= dt * heart_sprite.dy
    else:
        heart_sprite.y += dt * heart_sprite.dy

def center_image(image):
    """ Sets an image's anchor point to its centre """
    image.anchor_x = image.width // 2
    image.anchor_y = image.height // 2

# Make window.
window = pyglet.window.Window(width=640, height=480)

# Set image path.
pyglet.resource.path = ['images']
pyglet.resource.reindex()

# Load images.
heart_img = pyglet.resource.image('red-heart.png')
cupcake_img = pyglet.resource.image('cupcake.png')
s_img = pyglet.resource.image('S.png')
# Add all letters here ...
t_img = pyglet.resource.image('t.png')

# Center images.
center_image(heart_img)
center_image(cupcake_img)
center_image(s_img)
# Centre all letters here ...
center_image(t_img)

# Make sprites.
half_window_height = window.height // 2

heart_sprite = pyglet.sprite.Sprite(img=heart_img, x=100, y=300)
# Set Sprite's speed.
heart_sprite.dx = 200
heart_sprite.dy = 90

cupcake_sprite = pyglet.sprite.Sprite(img=cupcake_img, x=550, y=half_window_height)
s_sprite = pyglet.sprite.Sprite(img=s_img, x=100, y=half_window_height)
# Make all letters into Sprites and adjust the x-axis coordinates...
t_sprite = pyglet.sprite.Sprite(img=t_img, x=310, y=half_window_height)

# Calculate midpoints.
# Here the midpoint between the 'bouncing point' and the 'landing  point' is calculated.
# This is done for all bounces the Sprite makes. 
MIDPOINTS.append(calculate_midpoint(s_sprite, t_sprite))
MIDPOINTS.append(calculate_midpoint(t_sprite, cupcake_sprite))
# The 'landing  point' must be saved to be able to determine when one bounce has finished
# and move on to the next.
ENDPOINTS.append(t_sprite.x)
ENDPOINTS.append(cupcake_sprite.x)

# Rescale sprites.
heart_sprite.update(scale=0.05)
cupcake_sprite.update(scale=0.1)
s_sprite.update(scale=0.3)
# Resize all letter Sprites here ...
t_sprite.update(scale=0.3)

@window.event
def on_draw():
    window.clear()
    cupcake_sprite.draw()
    heart_sprite.draw()
    s_sprite.draw()
    # Draw all letter Sprites here ...
    t_sprite.draw()

@window.event
def on_mouse_press(x, y, button, modifiers):
    """
    I only put the schedule_interval inside a mouse_press event so that I can control
    when the animation begins by clicking on it. Otherwise the last line in this method
    can be placed directly above the 'pyglet.app.run()' line. This would run the 
    animation automatically when the app starts.
    """
    # Call update 60 times a second
    pyglet.clock.schedule_interval(update, 1/60.)

if __name__ == '__main__':
    pyglet.app.run()

所以主要问题是 Animation 假定在大图像中包含一系列图像。它被称为 sprite 动画,它本质上只是您想要的运动的一系列条带(通常在一行或网格模式中)。它对于动画行走、攻击和其他类似的游戏机制很有用。

但是要在 canvas 周围移动 object,您需要以某种方式手动操作顶点或图像位置。您自己的解决方案的工作原理是检查 X 是否大于或小于 minmax 限制。我只想在此基础上添加一些技巧,以便更轻松、更快速地处理动作和方向。下面我使用 bitwise operations 来确定运动方向,这使得心脏在 parent (window) 宽度和高度限制附近反弹。

我还冒昧地通过将 pyglet Window class 继承为一个 object/class 并使 heart 它自己的 class 更容易区分什么时候调用什么 object.

from pyglet import *
from pyglet.gl import *

key = pyglet.window.key
# Indented oddly on purpose to show the pattern:
UP    = 0b0001
DOWN  = 0b0010
LEFT  = 0b0100
RIGHT = 0b1000

class heart(pyglet.sprite.Sprite):
    def __init__(self, parent, image='heart.png', x=0, y=0):
        self.texture = pyglet.image.load(image)
        pyglet.sprite.Sprite.__init__(self, self.texture, x=x, y=y)

        self.parent = parent
        self.direction = UP | RIGHT # Starting direction

    def update(self):
        # We can use the pattern above with bitwise operations.
        # That way, one direction can be merged with another without collision.
        if self.direction & UP:
            self.y += 1
        if self.direction & DOWN:
            self.y -= 1
        if self.direction & LEFT:
            self.x -= 1
        if self.direction & RIGHT:
            self.x += 1

        if self.x+self.width > self.parent.width:
            self.direction = self.direction ^ RIGHT # Remove the RIGHT indicator
            self.direction = self.direction ^ LEFT # Start moving to the LEFT
        if self.y+self.height > self.parent.height:
            self.direction = self.direction ^ UP # Remove the UP indicator
            self.direction = self.direction ^ DOWN # Start moving DOWN
        if self.y < 0:
            self.direction = self.direction ^ DOWN
            self.direction = self.direction ^ UP
        if self.x < 0:
            self.direction = self.direction ^ LEFT
            self.direction = self.direction ^ RIGHT

    def render(self):
        self.draw()

# This class just sets up the window,
# self.heart <-- The important bit
class main(pyglet.window.Window):
    def __init__ (self, width=800, height=600, fps=False, *args, **kwargs):
        super(main, self).__init__(width, height, *args, **kwargs)
        self.x, self.y = 0, 0

        self.heart = heart(self, x=100, y=100)

        self.alive = 1

    def on_draw(self):
        self.render()

    def on_close(self):
        self.alive = 0

    def on_key_press(self, symbol, modifiers):
        if symbol == key.ESCAPE: # [ESC]
            self.alive = 0

    def render(self):
        self.clear()

        self.heart.update()
        self.heart.render()
        ## Add stuff you want to render here.
        ## Preferably in the form of a batch.

        self.flip()

    def run(self):
        while self.alive == 1:
            self.render()

            # -----------> This is key <----------
            # This is what replaces pyglet.app.run()
            # but is required for the GUI to not freeze
            #
            event = self.dispatch_events()

if __name__ == '__main__':
    x = main()
    x.run()

基本原理是一样的,操纵sprite.x横向移动,sprite.y纵向移动。还有更多的优化要做,例如,更新应该根据上次渲染进行缩放。这样做是为了避免在您的显卡跟不上时出现故障。它会很快变得非常复杂,所以我将 example 告诉你如何计算这些运动。

此外,您可能想要渲染一个批次,而不是直接渲染精灵。这将大大加快大型项目的渲染过程。

如果您不熟悉按位运算,一个简短的描述是它在 bit/binary 级别上运行(例如 4 == 0100),并在 XOR 上进行运算UPDOWNLEFTRIGHT 的值。例如,我们可以通过合并 01000001 得到 0101 来 add/remove 方向。然后我们可以执行二进制 AND(不像传统的 and 运算符)通过执行 self.direction & 0100 如果它是 True,将导致 1。如果您愿意,这是检查 "states" 的便捷方法。