I replaced the first tile row+column that served to set a "size" to the scrollable container (because the container without layout has no size?) but black pixel lines (1x1000 and 1000x1). It works OK. Then, I cleaned the code a bit.
firstLine := container.NewHBox()
horizontalBorder := canvas.NewImageFromFile("static/black_hline.png")
horizontalBorder.FillMode = canvas.ImageFillOriginal
horizontalBorder.Resize(fyne.NewSize(float32(mapMaxX-1)*32, 1))
firstLine.Add(horizontalBorder)
verticalBorder := canvas.NewImageFromFile("static/black_vline.png")
verticalBorder.FillMode = canvas.ImageFillOriginal
verticalBorder.Resize(fyne.NewSize(1, float32(mapMaxY-1)*32))
secondLine := container.NewHBox(verticalBorder)
Finally, I made my first interactivity but listening to keystrokes and changing player position.
[...]
window.Canvas().SetOnTypedKey(mapKeyListener)
[...]
func mapKeyListener(event *fyne.KeyEvent) {
if event.Name == fyne.KeyUp {
playerPosY = playerPosY - 1
} else if event.Name == fyne.KeyDown {
playerPosY = playerPosY + 1
} else if event.Name == fyne.KeyLeft {
playerPosX = playerPosX - 1
} else if event.Name == fyne.KeyRight {
playerPosX = playerPosX + 1
}
drawPlayer()
}
func drawPlayer() {
playerAvatar := canvas.NewImageFromFile("./static/warrior.png")
playerAvatar.FillMode = canvas.ImageFillOriginal
playerAvatar.Resize(fyne.NewSize(32, 32))
playerAvatar.Move(fyne.NewPos(float32(playerPosX*32), float32(playerPosY*32)))
mapContainer.Add(playerAvatar)
}
It doesn't work super well because you have to scroll to refresh the screen after a keystroke and previous position is not removed.
By rewriting a bit of code here and there (mostly, adding playerAvatar as a package var and only moving, not adding it to the mapContainer every time), I now have a working character that can be moved on the whole map and that can't get out of it :).
The only remaining issue is the map horizontal + vertical borders that help me make the containerWithoutLayout scrollable aren't of the good size. I could leave them exactly the right size but that would prevent me from making maps from different sizes.
So I rewrote this part (again!) to make a Hbox + VBox and adding just enough 32x1 and 1x32 pixel lines to fit the whole map (horizontalBorder & verticalBorder). Crude...
mapContainer = container.NewWithoutLayout()
for i := 0; i < mapMaxY; i++ {
firstLine.Add(horizontalBorder)
currentLine := float32(i) * 32
for j := 0; j < mapMaxX; j++ {
if j == 0 {
verticalBorder.Add(verticalLine)
}
tile := imageMatrix[i][j]
tile.Resize(fyne.NewSize(32, 32))
currentPos := fyne.NewPos(currentLine, float32(j)*32)
tile.Move(currentPos)
mapContainer.Add(tile)
}
}
After adding some new tiles, I needed to extract all map logic in another package. Even if I want to have maps that are generated randomly, some other maps are predetermined.
Tiles should then be stored in some way. To do this in a simple way, I've added a table called TilesTypes and map is a matrix with a number pointing on one of those tiles types.
TilesTypes = []string{
"./static/grass.png",
"./static/building_corner_left_up.png",
"./static/building_corner_right_up.png",
"./static/building_corner_right_down.png",
"./static/building_corner_left_down.png",
"./static/building_left.png",
"./static/building_down.png",
"./static/building_right.png",
"./static/building_up.png",
"./static/opaque.png",
}
[...]
Map1 = [][]int{
[...]
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 5, 5, 5, 5, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 9, 9, 9, 9, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 9, 9, 9, 9, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 9, 9, 9, 9, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 9, 9, 9, 9, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 7, 7, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
[...]
The result is pretty clean 😎.
There still is change to do to make it impossible for player to walk on some tiles (walls).
to do this I changed the TilesTypes from a string table to a Struct
type TileInfo struct {
Path string
IsWalkable bool
}
And I made some changes on mapKeyListener to check "walkability" before moving, in addition to checking we are not at the edge of the map. Probably will move the out of the map logic inside this function as well.
func checkWalkable(futurePosX int, futurePosY int) {
if maps.TilesTypes[currentMap[futurePosX][futurePosY]].IsWalkable {
playerPosX = futurePosX
playerPosY = futurePosY
}
}
Now, you can't moved across walls :D
But now, I have a lot of individual tiles (and probably more coming) and they are all coming from the same source file. This is a bit dump, and I tried to find a way to extract the tiles I want by creating subimages of a bigger image.
That's how I managed to do it :
type Coord struct {
X, Y int
}
type TileInfo struct {
Coordinates Coord
IsWalkable bool
}
var (
TilesTypes = []TileInfo{
{Coordinates: Coord{X: 0, Y: 64}, IsWalkable: true}, //0
{Coordinates: Coord{X: 576, Y: 96}, IsWalkable: false}, //1
[...]
func extractTileFromTileset(coord Coord) (image.Image, error) {
x, y := coord.X, coord.Y
file, err := os.Open("static/RPG Nature Tileset.png")
if err != nil {
fmt.Println("Error opening image:", err)
return nil, err
}
defer file.Close()
bigImage, _, err := image.Decode(file)
if err != nil {
fmt.Println("Error decoding image:", err)
return nil, err
}
width := 32
height := 32
partImage := image.NewRGBA(image.Rect(0, 0, width, height))
draw.Draw(partImage, partImage.Bounds(), bigImage, image.Point{x, y}, draw.Src)
return partImage, nil
}
Instead of pointing to a file, I pointed to coordinates on the main tileset file, and generate a subimage. Returned partImage is of image.Image type, which fyne supports if you use NewImageFromImage() instead of NewImageFromFile().
That's huge for future map creation process.
This also made me discover that I had mixed up all the axes, both in checkWalkable and mapContainer creation. Too many of i/j x/y h/v columns/rows...
Reworked mapKeyListener, checkWalkable and movePlayer to prepare for adding my first PNJ! checkWalkable only check if path is walkable and is generic (both player and PNJs). Doing something (only moving is implemented so far) triggers a new turn for PNJs (only moving for now as well). Farmer movement is random.
First implementation is very crude but it works well :)