Soul Reaver
Soul Reaver

Reputation: 73

How to fix aspect ratio of a kivy game?

I've started playing with Kivy, but I'm rather new to all this and am already struggling.

I'm trying to make a board game - I want the window to be resizeable, but I don't want resizing the window to mess up the aspect ratio of the game (so in other words, I want the window to have black bars above or to the sides of the content if the window is resized to something other than the intended aspect ratio of the content)

The easiest way I could see to ensure this is to either:

a) Lock the aspect ratio of the Window itself so it's always 10:9 (the aspect ratio of the board and all on-screen elements) - even when fullscreen.

or

b) Use some sort of widget/surface/layout that is centered on the window and has a locked 10:9 aspect ratio. I then subsequently use this as the base onto which all my other images, widgets, grids etc are placed.

However, I really don't know how to do either of these. I'm not sure if I can lock the Window aspect ratio, and the only kivy-specific object I've found that lets me lock the aspect ratio is from kivy.graphics.image... which I don't seem to be able to use as a 'base' for my other stuff.

EDIT: So far I've written the code below: it creates a layout (and colours it slightly red) that 'fixes' its own aspect ratio whenever it is resized. However, it still isn't centered in the window, and more problematic, it results in an endless loop (probably because the aspect fix code corrects the size, but then kivy 'corrects' its size back to being the size of the window, triggering the aspect fix again, thought I could be wrong).

EDIT: Modified the code again, but it's still an endless loop. I though that referring only to the parent for size info would fix that, but apparently not.

I'd appreciate anyone helping me fix my code.

Code below:

test.py

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.image import Image
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.properties import (ObjectProperty,
                             NumericProperty,
                             ReferenceListProperty)
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Rectangle

class Board(AnchorLayout):
    pass

class BackgroundLayout(RelativeLayout):
    def FixAspectRatio(self, *args):
        correctedsize = self.parent.size
        if correctedsize[0] > correctedsize[1]*(10/9):
            correctedsize[0] = correctedsize[1]*(10/9)
        elif correctedsize[0] < correctedsize[1]*(10/9):
            correctedsize[1] = correctedsize[0]/(10/9)
        return correctedsize

class test(App):
    game = ObjectProperty(None)

    def build(self):
        self.game = Board()
        return self.game

if __name__ == '__main__':
    test().run()

test.kv

<Board>
    BackgroundLayout:
        canvas.before:
            Color:
                rgba: 1, 0, 0, 0.5
            Rectangle:
                size: self.size
                pos: self.pos
        size: self.FixAspectRatio(self.parent.size)
        pos: self.parent.pos

Upvotes: 2

Views: 4077

Answers (2)

Soul Reaver
Soul Reaver

Reputation: 73

I managed to find a solution I was happy with with a lot of help from kived from the kivy chat.

The below bases the game bounds on a 'background' image, then places a Relative Layout over this background image. For all subsequent children, the coordinate (0, 0) will refer to the bottom left of the (aspect locked) background image, and they can use the 'magnification' property of the Background to adjust their size/position according to the screen size.

Provided any gamebounds.png image, the below code will implement this. Note that the RelativeLayout is shaded red (with 50% transparency) just to show its position (and show that it matches the gamebounds.png image size and position at all times):

test.py

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.image import Image
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.properties import (ObjectProperty,
                             NumericProperty,
                             ReferenceListProperty,
                             ListProperty)
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Rectangle

class Game(FloatLayout):
    pass

class Background(Image):
    offset = ListProperty()
    magnification = NumericProperty(0)


class Bounds(RelativeLayout):
    pass

class test(App):
    game = ObjectProperty(None)

    def build(self):
        self.game = Game()
        return self.game

if __name__ == '__main__':
    test().run()

test.kv

<Game>
    Background:
        id: BackgroundId
        #Provide the image that will form the aspect-locked bounds of the
        #game.  Image widgets have aspect_lock set to True by default.
        source: 'gamebounds.png'
        #All the image to stretch to fill the available window space.
        allow_stretch: True
        size: self.parent.size
        #Find the coordinates of the bottom-left corner of the image from
        #the bottom-left corner of the window.  Call this the 'offset'.
        offset: [self.center_x - (self.norm_image_size[0]/2),
        self.center_y - (self.norm_image_size[1]/2)]
        #Find out the factor by which the image is magnified/shrunk from
        #its 'default' value:
        magnification: self.norm_image_size[0] / self.texture_size[0]

        Bounds:
            #The canvas below isn't needed, it's just used to show the 
            #position of the RelativeLayout
            canvas:
                Color:
                    rgba: 1, 0, 0, 0.5
                Rectangle:
                    size: self.size
                    pos: (0, 0)
            #Set the position of the RelativeLayout so it starts at the
            #bottom left of the image
            pos: self.parent.offset
            #Set the size of the RelativeLayout to be equal to the size of
            #the image in Background
            size: self.parent.norm_image_size

Upvotes: 0

tito
tito

Reputation: 13251

One approach would be to create a Layout that always maximize the children given an aspect ratio. Here is an example::

from kivy.lang import Builder
from kivy.app import App
from kivy.uix.relativelayout import RelativeLayout
from kivy.properties import NumericProperty

kv = """
ARLayout:
    Widget:
        canvas:
            Color:
                rgb: 1, 0, 0
            Rectangle:
                size: self.size
                pos: self.pos
"""

class ARLayout(RelativeLayout):
    # maximize the children given the ratio
    ratio = NumericProperty(10 / 9.)

    def do_layout(self, *args):
        for child in self.children:
            self.apply_ratio(child)
        super(ARLayout, self).do_layout()

    def apply_ratio(self, child):
        # ensure the child don't have specification we don't want
        child.size_hint = None, None
        child.pos_hint = {"center_x": .5, "center_y": .5}

        # calculate the new size, ensure one axis doesn't go out of the bounds
        w, h = self.size
        h2 = w * self.ratio
        if h2 > self.height:
            w = h / self.ratio
        else:
            h = h2
        child.size = w, h


class TestApp(App):
    def build(self):
        return Builder.load_string(kv)

TestApp().run()

Upvotes: 3

Related Questions