godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
90.94k stars 21.15k forks source link

Godot is not checking the subtypes in typed arrays #95113

Open jeadhz opened 3 months ago

jeadhz commented 3 months ago

Tested versions

Reproducible in 4.3.rc2

System information

Godot v4.3.rc2 - Windows 10.0.19045 - GLES3 (Compatibility) - NVIDIA GeForce GT 1030 (NVIDIA; 32.0.15.5599) - Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz (8 Threads)

Issue description

I have this base class for all blocks in my game:

Block extends Resource

And I have this 2 derived from it:

Wall extends Block
Tile extends Block

I store each of them like this:

walls: Array[Wall]
tiles: Array[Tile]

These blocks have to be registered in the game before you can place them, but the thing is that you cannot assign an Array[Wall] to an Array[Block] even though the walls are derived from blocks

capture capture2

In conclusion, you can do:

Array[Wall] = walls
some_wall: Wall = ...
some_block: Block = some_wall

but not:

Array[Block] = walls

or:

Array[Resource] = walls
Array[Resource] = blocks

It doesn't take into account inheritance ~ . ~

Steps to reproduce

Copy and paste this code:

var textures_2d: Array[Texture2D] = []
var all_textures: Array[Texture] = textures_2d

Or this one:

var textures_2d: Array[Texture2D] = []
do_stuff_with_textures(textures_2d)

func do_stuff_with_textures(textures: Array[Texture]) -> void:
    print(textures)

It's not exactly the same as in my project but it works in the same way

Minimal reproduction project (MRP)

n/a

theraot commented 3 months ago

Currently when you pass or assign an array, it is a reference.

So, if you do something like this:

func test() -> void:
    var a := Wall.new()
    var b := Wall.new()
    var c := Wall.new()
    var walls:Array[Wall] = [a, b, c]
    var blocks:Array[Block] = walls # ERROR HERE

class Block:
    var x:int

class Wall extends Block:
    var y:int

The semantics is that you were to have a reference to the same array in both walls and blocks variables. But blocks has a type that would allow to add to the array things that are not Wall (such as an instance of another class that extends Block, such as Tile from your example), and thus breaking the type of walls.


You could set a copy instead:

func test() -> void:
    var a := Wall.new()
    var b := Wall.new()
    var c := Wall.new()
    var walls:Array[Wall] = [a, b, c]
    var blocks:Array[Block]
    blocks.assign(walls)

class Block:
    var x:int

class Wall extends Block:
    var y:int

So, as far as I can tell, this is working as intended. Altough I admit this has biten me in the past. In particular, it is not expected if you come from a language where arrays are value types and have variance by default (which is easy in that case, because when arrays are value types you are always making a copy).

I suppose there are other ways Godot could handle this... The advantage of the current solution is that you are in control of when the copy happens. If you have some idea for an alternative approach, you might consider opening a proposal at https://github.com/godotengine/godot-proposals/issues However, be aware that a breaking change wold delay its implementation.


Anyway, I think the error message could be updated to suggest using assign.

jeadhz commented 3 months ago

I'm aware of that solution. Thank you, I use it a lot. My problem comes when you try to pass that array as an argument (sorry if I was not very clear when explaining it)

For example:

placed_walls: Array[Wall]

remove_blocks(placed_walls)  # Error here

func remove_blocks(Array[Block]) -> void: ...

The issue is that you can't pass any other than the same exact type to a typed array, even though the elements could be a subtype of it

Hope I've made myself understood ^^