Ship, Captain, and Crew: Scaling the Screen

diceGameScalescript

So, now that I’ve completed BornCG’s great 3D game tutorial, I’m ready to set out on my own! Of course, I already mentioned what the game would be about, it is a remake of the classic “Ship, Captain, and Crew” dice game. I’ve already made a 2D version of this game as an Android app, but now I’m trying to develop some 3D skills by remaking the game using Godot’s 3D engine.

I have several goals for this game:

  1. 3D – I know, it seems kind of obvious, but I never made a 3D game on my own before! (No tutorial, help, etc.)
  2. Featuring all of my own 3D models – I want to personally build each 3D model to enhance my limited Low Poly Blender skills.
  3. Cross platform – I would like to be able to play this game on a Linux, Windows, and Android OS.
  4. Open Source – It is important to me that this project is open sourced to the public for any to view, edit, copy, and use. Perhaps my meager and fledgling skills will help some other newbie to create something better than I did.

So, with that in mind, my first goal was to tackle screen size. Once before, I made a 2D side scroller called Critical Velocity using the libGDX library, and I found out after I made the game that I needed to work out different screen sizes. It was really hard to do so after the fact, and so I wanted to start with that here, so I don’t run into the same problem.

I found several guides on how one might accomplish this in Godot. Some just moved the camera further or closer based on the size of the screen. Some used the stretch mode. Others scaled objects to meet the criteria. And yet others had different assets to use for various sizes. So, after viewing all three, here is what I came up with:

First, I made a global.gd script that is automatically loaded as soon as the game starts. It looks like this:

extends Node

var my_viewport_scale = 1
var actualScreenRatio = 1.777777
var numberOfPlayers = 1
var computerPlayer = 0

func _ready():
randomize()
var viewport = get_node(“/root”).get_children()[1].get_viewport_rect().size
# comment the below call for OS.get_screen_size() if you want to force a
# certain screen size. If this call is not commented out, it will use the
# native screen size of the device or computer, unless it is greater than
# the Godot project settings of 1920*1080.
viewport = OS.get_screen_size()
# print(viewport) # Reference only

actualScreenRatio = viewport.x / viewport.y

my_viewport_scale = viewport.y/1080

So, in this script, I am asking the operating system for the size of the screen. I am building the entire game in a 1920×1080 resolution, but I use this math to find the screen ratio, and the screen size. I then set a scale based on the y axis ( my research seemed to point to that one being the best to base off of, but I don’t know particularly why ). Once these variables are in place, I can put this at the beginning of any “scene” that I make:

func _ready():
# My way to change the scale for every screen size. This allows me to build
# in 1920 x 1080 resolution, and scale it for bigger or smaller displays.
# This works for both 16:9 and 4:3 screen ratios, although 4:3 may not have
# the proper look.
# Tested satisfactory on: 800×480, 854×480, 800×600, 960×540, 1024×600, 1280×768, 1280×720, 1280×800, 1824×1200, 1920×1080
# Did not work well on 1400×900.

var new_y_scale = get_node(“/root/global”).my_viewport_scale
var new_x_scale = new_y_scale

if get_node(“/root/global”).actualScreenRatio < 1.4 :
new_x_scale = new_y_scale * ((800/600) / get_node(“/root/global”).actualScreenRatio)
elif get_node(“/root/global”).actualScreenRatio < 1.61 :
new_x_scale = new_y_scale / (get_node(“/root/global”).actualScreenRatio/1.45)
elif get_node(“/root/global”).actualScreenRatio < 1.76 :
new_x_scale = new_y_scale / (get_node(“/root/global”).actualScreenRatio/1.54)
else :
new_x_scale = new_y_scale

get_node(“/root”).get_children()[1].set_scale(Vector2(new_x_scale,new_y_scale))
# print(get_node(“/root”).get_children()[1].get_scale()) # Reference only.

In the title screen scene, I use the “ready” function, which is called as soon as a scene launches, to set the scale of that scene/node. By doing this, everything in that scene is not directly scaled, but is automatically set to scale as the whole view is scaled. This saves me from scaling each object, but rather scaling the view as a whole. At least, as far as I understand it in my limited knowledge of Godot and 3D programming, and it seems to work well.

The key part being that I want to check the actual screens ratio. Once I have that actual ratio from the global script, I can edit the scenes scale to match (at least closely) the ratio of the screen, and scale appropriately. This allows the game to be viewed on a 4:3 ratio screen (like an older computer), and a 16:9 screen (like most phones and modern computers), and several odd phone screen sizes.

As you can see in my notes, I tested this out on various screen sizes, and it worked well on a lot of them. This does have one downside, as it will make some screen resolutions look “squished” a tiny bit. But, with my limited repository of Godot/3D knowledge, this will do for now.

Here is an example from a 3D scene, the game table:

func _ready():
var new_y_scale = get_node(“/root/global”).my_viewport_scale
var new_x_scale = new_y_scale
var new_z_scale = new_y_scale

if get_node(“/root/global”).actualScreenRatio < 1.4 :
new_x_scale = new_y_scale * ((800/600) / get_node(“/root/global”).actualScreenRatio)
elif get_node(“/root/global”).actualScreenRatio < 1.61 :
new_x_scale = new_y_scale / (get_node(“/root/global”).actualScreenRatio/1.45)
elif get_node(“/root/global”).actualScreenRatio < 1.76 :
new_x_scale = new_y_scale / (get_node(“/root/global”).actualScreenRatio/1.54)
else :
new_x_scale = new_y_scale

get_node(“/root”).get_children()[1].set_scale(Vector3(new_x_scale,new_y_scale,new_z_scale))
# And set the HUD scale…
get_node(“ControlHUD”).set_scale(Vector2(new_x_scale,new_y_scale))
get_node(“winControl”).set_scale(Vector2(new_x_scale,new_y_scale))
# print(get_node(“/root”).get_children()[1].get_scale()) # Reference only.

Most looks the same, with the addition of scaling the “HUD” display. In the 3D scene, I over-layed a control node with essentially a 2D scene where the controls and such are floating above the game and within view for the player to use. Of course there is also another axis to change, the “z” axis, which is added in as well.

So, perhaps there is a better way to do this, but it seems to work pretty well for me. I also don’t notice any sort of performance hit doing this, as it only scales once at the scene change, and the scenes seem to load instantly. So I’ll stick with this for now. If you want to check out the project as a whole, you can find it on my GitLab.

Linux – keep it simple.

Water drums with big hands?

hand

Okay, so the post title does sound a little weird. But it’s not. Actually, it’s really simple. What is the biggest problem with designing video games? For me personally, it is making sure that it looks exactly the way you want it too on any sized screen.

In my new water drum game, I’ve drawn a cool little hand for the mouse cursor. It is 200 x 200 pixels. On my 1280×1024 screen, that seems appropriate for this game. However, when I want to show it to my buddy who only has a 1920×1600, the size of the hand becomes disproportionately smaller than it was on my screen. On his screen it is only 1.3% of the screen size, but on mine it is doubled at 2.8% of the screen size.

What if someone tried to play the game on a 640×480 screen? Well, aside from consoling them for having such a tiny, poor resolution screen, the game could actually get a bit difficult to play as the mouse pointer would take up nearly 14%  (almost 1/6th) of the screen! What can you do? Then your game would have to be purposely designed for each screen size.

There are several options.

  1. Purposely build and distribute the game for a set screen size. You would then need to name that size as a dependency of minimum resolution, and use a window for larger screens.
  2. Build your game to check for common screen resolutions, and have multiple artwork and if/then statements to check which ones to use. (I’ve done this in the past with some Android games.)
  3. Write code to scale your images to be appropriate for the screen resolution.
  4. Some combination of the above. (My Dad always picked “D – all of the above” if it was an option, so I’m putting it in here.)

Each of these has drawbacks as well as perks. If you do the first choice, your game will look absolutely best on that resolution. This can be limiting to those with lower res systems, or will limit you to make a low res game that will look bad or tiny on hi res systems if in a window.

If you do the second, you will increase the size of your application by including bigger and better art for each resolution. Also, this gets tedious. What if you change the player icon? You now have to change it 18 times to support the change across each resolution, because currently there are 18 different “HD” resolutions. Not to mention lower res systems, which you probably just wont support anyways.

If you try the third, that of code to scale your graphics, it will always look proportional on any resolution. However, scaling may make the art look pixelated or stretched if scaled too far. Generally, this is the easiest, but may cause graphical quality problems.

I tend to think that most big games (at least on Android) use a combination of both. You select a few common screen sizes and make art for those, scaling anything in between with the closest matching resolution art.

What did I choose? Well, I chose to scale my art. I’m not too worried about quality of the art, since I drew it in a few minutes with pretty low quality to begin with. Here’s how I set the scale:

// Hide the mouse pointer and replace it with drum hand
window.setMouseCursorVisible(false);
Texture textureDrumHand;
textureDrumHand.loadFromFile(“graphics/hand.png”);
Sprite spriteDrumHand;
spriteDrumHand.setTexture(textureDrumHand);
spriteDrumHand.setOrigin(100, 100);
spriteDrumHand.setScale((resolution.x*.04)/200,(resolution.x*.04)/200);

By making the size of the hand a math problem derived from the resolution, it *should* be right on different sized screens. In theory, it should always be about 4% of the screen width. It is 4% of screen width divided by 200 (the size of the png file), which will make the scale, which is multiplied by the actual size of the png file. Mathematically, that means we can take out the divide by 200 and multiply by 200, so it will remain as 4% of the screen resolution!

Similarly, I did the same for the water drums themselves, except I want them to be 20% of the screen resolution width:

// And the water drums themselves.
Sprite spriteWD1;
spriteWD1.setTexture(textureWaterDrum_still);
spriteWD1.setPosition(resolution.x/15*2,resolution.y/7*2);
spriteWD1.setScale((resolution.x*.2)/500,(resolution.x*.2)/500);

Again, the formula is simple: (resolution.x * {desired percentage}) / {original art size in pixels}. This way it will always be a uniform size no mater what screen resolution you use!

You can check out the full commit on my GitLab if you like!

Linux – keep it simple.