Compare commits

...

10 Commits

Author SHA1 Message Date
Sean Hickey 662517d1de Use fullscreen mode. Fix some linter issues 2023-04-30 00:33:18 -07:00
Sean Hickey 2e0ac84163 linter stuff 2023-03-12 23:04:57 -07:00
Sean Hickey 06bd0cfd12 Add build time variables 2023-01-01 15:47:53 -08:00
Sean Hickey 460316aaf6 Restructure significantly, remove command/events for now. 2022-11-26 22:47:42 -08:00
Sean Hickey cee1d19f36 update readme to remove SDL 2022-11-22 00:20:14 -08:00
Sean Hickey ce5ff1a552 Rework entire project to use raylib.
Removed SDL entirely. Internally, raylib uses glfw and miniaudio, which
is the audio library I was going to use anyway.

I added a threadChannel that works very similar to the sdl.Do()
functions to support goroutines with a single-threaded library.
2022-11-22 00:08:00 -08:00
Sean Hickey f8d47107ce test playing music with beep 2022-11-21 20:04:24 -08:00
Sean Hickey d86124b735 switch to using gopackagebase 2022-11-20 15:38:28 -08:00
Sean Hickey 4882cdedf5 Remove pointers for concurrency 2022-11-20 15:20:37 -08:00
Cahley e20133d5d0 Added Delilah + King Animation sprites
replaced Percy penguin with Delilah in  main file.
2022-11-15 14:52:54 -08:00
55 changed files with 1063 additions and 1587 deletions

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}"
}
]
}

View File

@ -2,8 +2,7 @@ FROM alpine:latest
RUN apk add --no-cache \
gcc musl-dev make go \
alsa-lib libxrandr \
sdl2-dev sdl2_mixer-dev sdl2_image-dev sdl2_ttf-dev
alsa-lib libxrandr
WORKDIR /home

View File

@ -29,6 +29,7 @@ release_static: prebuild
CGO_LDFLAGS="-Wl,-rpath -L${HOME}/pkg/lib -L/usr/lib -L/usr/lib/x86_64-linux-gnu" \
${GO} ${GO_BUILD_RELEASE_STATIC}
lint: linter
linter:
golangci-lint run

View File

@ -2,11 +2,11 @@
This is some game project that we are working on for fun.
This project is written in Go using SDL2.
This project is written in Go using raylib.
## Compiling
See the [Setup Docs][1] for installing all the necessary dependencies. It's fairly bare-bones, only requiring SDL.
See the [Setup Docs][1] for installing all the necessary dependencies. It's fairly bare-bones, only requiring raylib (which bundles everything, only depending on base graphics libs like X11).
Then you can use the Makefile:
```sh
@ -23,10 +23,6 @@ A release build can be build with these extra flags:
go build -x -v -ldflags "-s -w"
```
## Running
You may need the SDL2 libraries in order to run the resulting binary. Copy the SDL2 *.dll or *.so files into the same directory as the output executable and run the program.
## Docs
For initial environment setup, see the [Setup Docs][1].

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,51 +0,0 @@
# Linux Static Release Build
Because we're depending on SDL and X11 libraries, we can't easily
build a completely statically-linked binary using musl unless we
recompile those library dependencies also using musl. I was able to
build something that was mostly statically-linked except for libc, but
then it didn't run in a base Fedora Docker container.
The workaround for this is to use something where everything is
already compiled with musl, like an Alpine Linux Docker container.
To build a fully statically-linked binary, use the `Dockerfile` in the
root directory.
```sh
docker build -t project-ely-build .
```
This should spit out an executable called `project-ely-static`.
## Attempts on Debian
I tried installing musl on Debian.
I installed it to my home directory. I also specified my pkgsrc version of GCC.
```sh
CC=$HOME/pkg/gcc10/bin/gcc
./configure --disabled-shared --prefix=$HOME/musl
make -j4
make install
```
Then I had to symlink the X11 include dir.
```sh
cd musl-install/include
ln -s /usr/include/X11 X11
```
I ran into an issue where it was unable to find many basic libraries
like Xrandr and asound. But then I hit that same issue with the Alpine
container =(
## References
Before giving up and using Alpine, I found these cool links that
suggest you could do this more easily if we only depended on the C std
library stuff, no external dependencies like X11 and SDL.
* https://honnef.co/posts/2015/06/statically_compiled_go_programs__always__even_with_cgo__using_musl/
* https://github.com/golang/go/issues/26492

View File

@ -1,16 +0,0 @@
# Windows Static Release Build
You can build a statically-linked binary on Windows that should run
without requiring the SDL `*.dll` files.
```sh
go build -x -v -trimpath -tags static -ldflags "-s -w -H windowsgui"
```
There is a script called [build-windows-static.sh][1] in the scripts
folder to make this easy from Git Bash.
The Makefile supports cross-compiling for this from Linux with the
`cross_windows` target.
[1]: scripts/build-windows-static.sh

View File

@ -1,4 +1,4 @@
# Go with SDL on everything else
# Go with raylib on everything else
Most any other operating system is Unix-like (e.g. Linux, Mac, BSD, Haiku, etc.), so they will all have very similar setups.
Each of these systems also has their own way of installing software, usually with their own package managers. I don't know all the exact names of each of these packages, but hopefully giving you some examples will help.
@ -8,7 +8,7 @@ You will need the following dependencies:
* Git
* Go (Golang)
* GCC
* SDL2 (with Image, TTF, and Mixer)
* X11 or Wayland
That's all, you may even have most of them installed already.
@ -16,10 +16,12 @@ I also use [golangci-lint][6] as a Go linter. This is used by the Makefile befor
### Debian
The [raylib-go readme][7] has good examples of installing the X11 or Wayland packages as needed. Below I just used the X11 packages as an example.
As the most common example, on Debian or Ubuntu, the following `apt` command should install all the dependencies:
```sh
sudo apt install git golang gcc libsdl2-dev libsdl2-mixer-dev libsdl2-image-dev libsdl2-ttf-dev
sudo apt install git golang gcc libgl1-mesa-dev libxi-dev libxcursor-dev libxrandr-dev libxinerama-dev
```
The default version of Go in the Debian repos might be too old. You can install a newer version using the testing/unstable repos (try [this][3] or [this][4]), or you could [manually install Go from their website][2].
@ -28,16 +30,16 @@ The default version of Go in the Debian repos might be too old. You can install
On Mac, the typical recommendation is to use [Homebrew][5]. The package names are pretty similar:
```sh
brew install git go gcc sdl2 sdl2_image sdl2_ttf sdl2_mixer
brew install git go gcc
```
### Redhat/Fedora/Rocky/CentOS
The Redhat-related distros all have very similar names as well, notably using `devel` instead of `dev` and `SDL2` is capitalized. I ran the following on Fedora successfully.
The Redhat-related distros all have very similar names as well, notably using `devel` instead of `dev`. I ran the following on Fedora successfully.
(these distros may use `dnf` or `yum` as the package manager executable, `dnf` is newer)
```sh
sudo dnf install git golang gcc SDL2-devel SDL2_image-devel SDL2_mixer-devel SDL2_ttf-devel
sudo dnf install git golang gcc mesa-libGL-devel libXi-devel libXcursor-devel libXrandr-devel libXinerama-devel
```
### pkgsrc
@ -48,10 +50,11 @@ As a reference for the BSD crowd, the dependencies in pkgsrc are:
* devel/git
* lang/go119
* lang/gcc12
* devel/SDL2
* graphics/SDL2_image
* fonts/SDL2_ttf
* audio/SDL2_mixer
* graphics/MesaLib
* x11/libXinerama
* x11/libXrandr
* x11/libXcursor
* x11/libXi
### Others
@ -72,3 +75,4 @@ or you can use `go build` directly.
[4]: https://unix.stackexchange.com/q/107689
[5]: https://brew.sh/
[6]: https://golangci-lint.run/usage/install/
[7]: https://github.com/gen2brain/raylib-go

View File

@ -1,9 +1,7 @@
# Go with SDL on Windows
# Go with raylib on Windows
Windows is a bit of a different beast when it comes to setting up a development environment. Hopefully this documentation helps get you started. This guide tries to detail everything you would need to download and configure to get this to work.
As a side note, there is a cool tool called `Scoop` that you could use to install a lot of these tools. However, I ran into an issue using the Scoop versions of SDL2 because the directory names were different from what the header files were expecting. Below I just installed everything manually instead.
## Install Git
If you don't have Git yet, you can download it here.
https://git-scm.com/
@ -21,7 +19,7 @@ You can use the Git Bash terminal to do this too. Open up `Git Bash`. This is a
```sh
mkdir projects
cd projects
git clone ssh://git@gitea.wisellama.rocks:222/Project-Ely/project-ely.git
git clone ssh://git@git.wisellama.rocks:222/Project-Ely/project-ely.git
```
## Install Go
@ -41,19 +39,6 @@ There is no installer for this, so we just need to put this in a folder that we'
Later on, we will need to set up the environment variables so that other software knows where we installed GCC.
## Install SDL2
Download the "devel mingw" versions of the following SDL2 releases (again, ideally the latest versions, just providing links for examples).
* SDL2 https://github.com/libsdl-org/SDL/releases/download/release-2.24.0/SDL2-devel-2.24.0-mingw.zip
* SDL2_Image https://github.com/libsdl-org/SDL_image/releases/download/release-2.6.2/SDL2_image-devel-2.6.2-mingw.zip
* SDL2_TTF https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.20.1/SDL2_ttf-devel-2.20.1-mingw.zip
* SDL2_Mixer https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.6.2/SDL2_mixer-devel-2.6.2-mingw.zip
Extract the zip files (you can use 7zip or the built-in Windows zip tool) to get a directory for each. They should look something like `SDL2_ttf-2.20.1`.
Then put these all in some folder you will remember, something like `C:\Users\Username\SDL2`.
You should end up with 4 folders inside the `SDL2` folder you created, one for each of the things we downloaded.
## Set environment variables
Now that we have all the software downloaded, we need to set some environment variables so that everything will know where we installed the software.
@ -71,14 +56,9 @@ Then we need to enable CGO (allowing the Go compiler to use a C compiler too):
CGO_ENABLED = 1
```
Then we need to set our "include" path. This is all of the directories that contain C header files to be included in our code. We need this to include the default GCC include directory as well as each of the SDL2 library include directories that we downloaded. This might be easier to copy-paste yourself if the versions are different. Also, change the `Username` to match your actual username (use search-replace to make this easier).
Then we need to set our "include" path. This is all of the directories that contain C header files to be included in our code. We need this to include the default GCC include directory. (Depending on how you installed GCC, this may already be set). Below is an example, but it would probably be easier to copy-paste your path to get the version information correct.
```
C_INCLUDE_PATH = C:\Users\Username\GCC\x86_64-12.1.0-release-win32-seh-rt_v10-rev3\mingw64\include;C:\Users\Username\SDL2\SDL2_image-2.6.2\x86_64-w64-mingw32\include;C:\Users\Username\SDL2\SDL2_mixer-2.6.2\x86_64-w64-mingw32\include;C:\Users\Username\SDL2\SDL2_ttf-2.20.1\x86_64-w64-mingw32\include;C:\Users\Username\SDL2\SDL2-2.24.0\x86_64-w64-mingw32\include;C:\Users\Username\SDL2\SDL2-2.24.0\x86_64-w64-mingw32\include\SDL2;
```
Then we need to set our "library" path. This is all of the directories that contain compiled library files (on Windows these are `*.dll` files) that can be imported into our code. This only needs to add all of the SDL2 directories.
```
LIBRARY_PATH = C:\Users\Username\SDL2\SDL2-2.24.0\x86_64-w64-mingw32\lib;C:\Users\Username\SDL2\SDL2_ttf-2.20.1\x86_64-w64-mingw32\lib;C:\Users\Username\SDL2\SDL2_mixer-2.6.2\x86_64-w64-mingw32\lib;C:\Users\Username\SDL2\SDL2_image-2.6.2\x86_64-w64-mingw32\lib;
C_INCLUDE_PATH = C:\Users\Username\GCC\x86_64-12.1.0-release-win32-seh-rt_v10-rev3\mingw64\include;
```
And depending on how you installed GCC, you may need to also add its `bin` directory to your path so that you will be able to call the `gcc` compiler. There are probably other entries already in your `PATH` variable, so just add this as an additional entry:
@ -86,11 +66,6 @@ And depending on how you installed GCC, you may need to also add its `bin` direc
PATH = C:\Users\Username\GCC\x86_64-12.1.0-release-win32-seh-rt_v10-rev3\mingw64\bin
```
## Copy the SDL2 libraries into your code directory
Your code may compile successfully, but it might not run because of the missing SDL2 libraries. Copy each of the `*.dll` library files into your code directory so that your resulting binary can load them correctly.
The DLL files should be in each of the `bin` directories of the SDL2 libraries that you downloaded.
## Build the software
Now we can just build the software and hope it works!
```

37
docs/setup/windows-sdl.md Normal file
View File

@ -0,0 +1,37 @@
# Setting up SDL on Windows
Note: this project no longer uses SDL, but I wanted to keep the documentation around on how to set up and configure SDL on Windows because it was particularly weird.
As a side note, there is a cool tool called `Scoop` that you could use to install a lot of these tools. However, I ran into an issue using the Scoop versions of SDL2 because the directory names were different from what the header files were expecting. Below I just installed everything manually instead.
## Install SDL2
Download the "devel mingw" versions of the following SDL2 releases (again, ideally the latest versions, just providing links for examples).
* SDL2 https://github.com/libsdl-org/SDL/releases/download/release-2.24.0/SDL2-devel-2.24.0-mingw.zip
* SDL2_Image https://github.com/libsdl-org/SDL_image/releases/download/release-2.6.2/SDL2_image-devel-2.6.2-mingw.zip
* SDL2_TTF https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.20.1/SDL2_ttf-devel-2.20.1-mingw.zip
* SDL2_Mixer https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.6.2/SDL2_mixer-devel-2.6.2-mingw.zip
Extract the zip files (you can use 7zip or the built-in Windows zip tool) to get a directory for each. They should look something like `SDL2_ttf-2.20.1`.
Then put these all in some folder you will remember, something like `C:\Users\Username\SDL2`.
You should end up with 4 folders inside the `SDL2` folder you created, one for each of the things we downloaded.
## Set environment variables
Now that we've downloaded SDL, we need to set some environment variables so that everything will know where we installed it.
We need to set our "include" path. This is all of the directories that contain C header files to be included in our code. We need this to include the default GCC include directory as well as each of the SDL2 library include directories that we downloaded. This might be easier to copy-paste yourself if the versions are different. Also, change the `Username` to match your actual username (use search-replace to make this easier).
```
C_INCLUDE_PATH = C:\Users\Username\GCC\x86_64-12.1.0-release-win32-seh-rt_v10-rev3\mingw64\include;C:\Users\Username\SDL2\SDL2_image-2.6.2\x86_64-w64-mingw32\include;C:\Users\Username\SDL2\SDL2_mixer-2.6.2\x86_64-w64-mingw32\include;C:\Users\Username\SDL2\SDL2_ttf-2.20.1\x86_64-w64-mingw32\include;C:\Users\Username\SDL2\SDL2-2.24.0\x86_64-w64-mingw32\include;C:\Users\Username\SDL2\SDL2-2.24.0\x86_64-w64-mingw32\include\SDL2;
```
Then we need to set our "library" path. This is all of the directories that contain compiled library files (on Windows these are `*.dll` files) that can be imported into our code. This only needs to add all of the SDL2 directories.
```
LIBRARY_PATH = C:\Users\Username\SDL2\SDL2-2.24.0\x86_64-w64-mingw32\lib;C:\Users\Username\SDL2\SDL2_ttf-2.20.1\x86_64-w64-mingw32\lib;C:\Users\Username\SDL2\SDL2_mixer-2.6.2\x86_64-w64-mingw32\lib;C:\Users\Username\SDL2\SDL2_image-2.6.2\x86_64-w64-mingw32\lib;
```
## Copy the SDL2 libraries into your code directory
Your code may compile successfully, but it might not run because of the missing SDL2 libraries. Copy each of the `*.dll` library files into your code directory so that your resulting binary can load them correctly.
The DLL files should be in each of the `bin` directories of the SDL2 libraries that you downloaded.

View File

@ -1,5 +0,0 @@
game.title = "Project Ely"
game.framerate = 60.0
log.file = "output.log"
log.writeToFile = false

9
go.mod
View File

@ -1,8 +1,9 @@
module gitea.wisellama.rocks/Project-Ely/project-ely
module git.wisellama.rocks/Project-Ely/project-ely
go 1.18
go 1.20
require (
gitea.wisellama.rocks/Wisellama/gosimpleconf v0.0.4
github.com/veandco/go-sdl2 v0.4.25
git.wisellama.rocks/Wisellama/gopackagebase v0.0.4
git.wisellama.rocks/Wisellama/gosimpleconf v0.1.0
github.com/gen2brain/raylib-go/raylib v0.0.0-20230413192425-0fdd3be3077b
)

10
go.sum
View File

@ -1,4 +1,6 @@
gitea.wisellama.rocks/Wisellama/gosimpleconf v0.0.4 h1:xFjG/dGPUoh1L7/PG9xQRr0GQZwlh1EGI9RnG8Ja8R8=
gitea.wisellama.rocks/Wisellama/gosimpleconf v0.0.4/go.mod h1:kY9gQL8laVTe+tW0ue5bYb6QThw78d7mx6AHwQ5CIzc=
github.com/veandco/go-sdl2 v0.4.25 h1:J5ac3KKOccp/0xGJA1PaNYKPUcZm19IxhDGs8lJofPI=
github.com/veandco/go-sdl2 v0.4.25/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
git.wisellama.rocks/Wisellama/gopackagebase v0.0.4 h1:EUj/GqcSLJVm4aedSFaleudXlJNwJqRuSrTE0LhcA1M=
git.wisellama.rocks/Wisellama/gopackagebase v0.0.4/go.mod h1:0xUyJkT61TTIpekmeApB8U0mVwNqX/6Iz85RKUZQYyU=
git.wisellama.rocks/Wisellama/gosimpleconf v0.1.0 h1:Z2FAzARct8ShV4NSueC/y+PyuSQVcyo4WnW7GoZ9L10=
git.wisellama.rocks/Wisellama/gosimpleconf v0.1.0/go.mod h1:Gg1vUTBRZD7qcXvdF8L50PsnL9coLt/XbWa5BwSDN/M=
github.com/gen2brain/raylib-go/raylib v0.0.0-20230413192425-0fdd3be3077b h1:a6MhRr2wZWGfWVu+hrnz+qkW/lGZzFPggpFJA8Zzj0I=
github.com/gen2brain/raylib-go/raylib v0.0.0-20230413192425-0fdd3be3077b/go.mod h1:AwtGA3aTtYdezNxEVbfchaLw/z+CuRDh2Mlxy0FbBro=

View File

@ -0,0 +1,21 @@
package animation
// AnimationMap maps each animation ID to the actual animation
var AnimationMap map[int]entityAnimation
// Enum containing IDs of all animations that exist to make them easy to reference.
const (
PenguinWalkRight int = iota
PenguinWalkLeft
PenguinWalkUp
PenguinWalkDown
PenguinStationaryRight
PenguinStationaryLeft
PenguinStationaryUp
PenguinStationaryDown
)
func DefineAnimations() {
AnimationMap = make(map[int]entityAnimation)
DefinePenguinAnimations()
}

View File

@ -0,0 +1,35 @@
package animation
import rl "github.com/gen2brain/raylib-go/raylib"
type animationTracker struct {
currentAnimation entityAnimation
animationStep int
}
func NewAnimationTracker() *animationTracker {
return &animationTracker{}
}
func (a *animationTracker) Draw(windowPosition rl.Vector2, color rl.Color) error {
step := a.animationStep / a.currentAnimation.GetSpeed()
err := a.currentAnimation.Draw(step, windowPosition, color)
if err != nil {
return err
}
a.animationStep = 1 + a.animationStep
return nil
}
func (a *animationTracker) SetAnimation(id int) {
newAnim := AnimationMap[id]
if newAnim != a.currentAnimation {
a.animationStep = 0
}
a.currentAnimation = newAnim
}

View File

@ -0,0 +1,68 @@
package animation
import (
rl "github.com/gen2brain/raylib-go/raylib"
)
type SpriteAnimation interface {
Draw(
frame int,
windowPosition rl.Vector2,
angle float32,
center rl.Vector2,
flip rl.Vector2,
color rl.Color,
) error
}
var (
FlipNone = rl.Vector2{X: 0, Y: 0}
FlipHorizontal = rl.Vector2{X: 1, Y: 0}
FlipVertical = rl.Vector2{X: 0, Y: 1}
FlipBoth = rl.Vector2{X: 1, Y: 1}
)
// An entityAnimation will take a SpriteAnimation and may manipulate it somehow (e.g. flipped for walking the other direction)
// This way you may cut down the actual number of pictures needed to define all the different animations.
type entityAnimation struct {
spriteAnimation SpriteAnimation
speed int
length int
angle float32
center rl.Vector2
flip rl.Vector2
}
func NewEntityAnimation(
spriteAnimation SpriteAnimation,
speed int,
length int,
angle float32,
center rl.Vector2,
flip rl.Vector2,
) entityAnimation {
return entityAnimation{
spriteAnimation: spriteAnimation,
speed: speed,
length: length,
angle: angle,
center: center,
flip: flip,
}
}
func (e entityAnimation) Draw(frame int, windowPosition rl.Vector2, color rl.Color) error {
return e.spriteAnimation.Draw(frame, windowPosition, e.angle, e.center, e.flip, color)
}
func (e entityAnimation) GetSpeed() int {
return e.speed
}
func getCenter(dimensions rl.Vector2) rl.Vector2 {
x := dimensions.X / 2.0
y := dimensions.Y / 2.0
return rl.Vector2{X: x, Y: y}
}

View File

@ -0,0 +1,85 @@
package animation
import (
"math/rand"
"git.wisellama.rocks/Project-Ely/project-ely/internal/sprite"
rl "github.com/gen2brain/raylib-go/raylib"
)
const (
PenguinDefault = PenguinStationaryRight
)
var penguinAnimations []int
func DefinePenguinAnimations() {
filename := sprite.DelilahWalking
penguinAnimations = make([]int, 0)
penguinAnimations = append(penguinAnimations, PenguinWalkRight)
penguinAnimations = append(penguinAnimations, PenguinWalkLeft)
penguinAnimations = append(penguinAnimations, PenguinWalkUp)
penguinAnimations = append(penguinAnimations, PenguinWalkDown)
penguinAnimations = append(penguinAnimations, PenguinStationaryRight)
penguinAnimations = append(penguinAnimations, PenguinStationaryLeft)
penguinAnimations = append(penguinAnimations, PenguinStationaryUp)
penguinAnimations = append(penguinAnimations, PenguinStationaryDown)
var (
dimensions rl.Vector2
offset rl.Vector2
center rl.Vector2
length int
speed int
border int
)
dimensions = rl.Vector2{X: 13, Y: 17}
// Walking Right is in the spritesheet.
speed = 5
offset = rl.Vector2{X: 0, Y: 1}
length = 5
border = 1 // optional border around each sprite
center = getCenter(dimensions) // center is for rotation, nil will default to w/2 h/2
walkRight := sprite.NewAnimation(filename, dimensions, offset, length, border)
AnimationMap[PenguinWalkRight] = NewEntityAnimation(walkRight, speed, length, 0, center, FlipNone)
// Walking Left is just that flipped.
AnimationMap[PenguinWalkLeft] = NewEntityAnimation(walkRight, speed, length, 0, center, FlipHorizontal)
// Stationary Right/Left is just the first frame.
length = 1
stationaryRight := sprite.NewAnimation(filename, dimensions, offset, length, border)
AnimationMap[PenguinStationaryRight] = NewEntityAnimation(stationaryRight, speed, length, 0, center, FlipNone)
AnimationMap[PenguinStationaryLeft] = NewEntityAnimation(stationaryRight, speed, length, 0, center, FlipHorizontal)
// Walk Up
length = 4
offset = rl.Vector2{X: 0, Y: 3}
walkUp := sprite.NewAnimation(filename, dimensions, offset, length, border)
AnimationMap[PenguinWalkUp] = NewEntityAnimation(walkUp, speed, length, 0, center, FlipNone)
// Stationary Up
length = 1
stationaryUp := sprite.NewAnimation(filename, dimensions, offset, length, border)
AnimationMap[PenguinStationaryUp] = NewEntityAnimation(stationaryUp, speed, length, 0, center, FlipNone)
// Walk Down
length = 4
offset = rl.Vector2{X: 0, Y: 0}
walkDown := sprite.NewAnimation(filename, dimensions, offset, length, border)
AnimationMap[PenguinWalkDown] = NewEntityAnimation(walkDown, speed, length, 0, center, FlipNone)
// Stationary Down
length = 1
stationaryDown := sprite.NewAnimation(filename, dimensions, offset, length, border)
AnimationMap[PenguinStationaryDown] = NewEntityAnimation(stationaryDown, speed, length, 0, center, FlipNone)
}
func RandomPenguinAnimation() int {
n := len(penguinAnimations)
i := rand.Intn(n)
return penguinAnimations[i]
}

View File

@ -0,0 +1,49 @@
package channels
import "runtime"
// Inspired by the go-sdl setup for allowing goroutines to coexist with a library that is single-threaded.
var (
// RayLib thread channel
RL = threadChannel{}
)
func init() {
runtime.LockOSThread()
}
// threadChannels allow you to guarantee that the given functions
// are all called sequentially to support a single-threaded library
// while still being able to use goroutines. Any function passed to
// Do() will be added to the call queue and run in order.
type threadChannel struct {
callQueue chan func()
mainFunc func(f func())
}
func (t *threadChannel) Main(main func()) {
t.callQueue = make(chan func())
t.mainFunc = func(f func()) {
done := make(chan bool, 1)
t.callQueue <- func() {
f()
done <- true
}
<-done
}
go func() {
main()
close(t.callQueue)
}()
for f := range t.callQueue {
f()
}
}
func (t *threadChannel) Do(f func()) {
t.mainFunc(f)
}

View File

@ -1,35 +0,0 @@
package config
import (
"os"
"gitea.wisellama.rocks/Wisellama/gosimpleconf"
)
var defaultConfig gosimpleconf.ConfigMap = gosimpleconf.ConfigMap{
"game.title": "Project Ely",
"game.framerate": "60.0",
"log.writeToFile": "false",
}
func Configure(filename string) (gosimpleconf.ConfigMap, error) {
var err error
_, err = os.Stat(filename)
if os.IsNotExist(err) {
// Config file does not exist, return a default configMap
return defaultConfig, nil
} else if err != nil {
return nil, err
}
configMap, err := gosimpleconf.Load(filename)
if err != nil {
return nil, err
}
flagMap := gosimpleconf.SetupFlagOverrides(configMap)
configMap = gosimpleconf.ParseFlags(configMap, flagMap)
return configMap, nil
}

View File

@ -1,43 +0,0 @@
package config
import (
"fmt"
"log"
"os"
"time"
)
type logWriter struct {
writeToFile bool
logFile *os.File
}
func (w *logWriter) Write(bytes []byte) (int, error) {
t := time.Now().UTC().Format(time.RFC3339)
return fmt.Fprintf(w.logFile, "%v %v", t, string(bytes))
}
func (w *logWriter) Cleanup() {
defer w.logFile.Close()
}
func SetupLogging(writeToFile bool, logFilename string) (*logWriter, error) {
var err error
log.SetFlags(0)
logFile := os.Stdout
if writeToFile {
logFile, err = os.OpenFile(logFilename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
}
writer := &logWriter{
writeToFile: writeToFile,
logFile: logFile,
}
log.SetOutput(writer)
return writer, nil
}

View File

@ -1,24 +0,0 @@
package config
import (
crypto_rand "crypto/rand"
"encoding/binary"
"fmt"
math_rand "math/rand"
)
// InitRNG will grab some cryptographically secure bytes to use as the
// seed for the non-crypto random number generator. Suggested on
// StackOverflow to avoid time-based seeds:
// https://stackoverflow.com/a/54491783
func InitRNG() error {
var b [8]byte
_, err := crypto_rand.Read(b[:])
if err != nil {
err = fmt.Errorf("error seeding random number generator: %w", err)
return err
}
math_rand.Seed(int64(binary.LittleEndian.Uint64(b[:])))
return nil
}

57
internal/entity/common.go Normal file
View File

@ -0,0 +1,57 @@
package entity
import (
rl "github.com/gen2brain/raylib-go/raylib"
)
type EntityAnimation interface {
Draw(step int, windowPosition rl.Vector2, color rl.Color) error
GetSpeed() int
}
type AnimationTracker interface {
Draw(windowPosition rl.Vector2, color rl.Color) error
SetAnimation(id int)
}
const (
// These line up with the VEC_DIRECTIONS slice
DirLeft int = iota
DirRight
DirUp
DirDown
)
// The following are axis direction vectors based on world coordinates.
// UP/DOWN is intentionally UP = positive (which is different from window coordinates)
var (
VecLeft = rl.Vector2{X: -1, Y: 0}
VecRight = rl.Vector2{X: 1, Y: 0}
VecUp = rl.Vector2{X: 0, Y: 1}
VecDown = rl.Vector2{X: 0, Y: -1}
VecDirections = []rl.Vector2{
// Prefer left/right animations by checking them first in the list
VecLeft,
VecRight,
VecUp,
VecDown,
}
)
func determineClosestDirection(velocity rl.Vector2) int {
closest := DirRight
max := float32(0)
buffer := float32(0.5) // This buffer lets us stick to the left/right animations for diagonal movement
for i, dir := range VecDirections {
dot := rl.Vector2DotProduct(velocity, dir)
if dot > max+buffer {
max = dot
closest = i
}
}
return closest
}

114
internal/entity/penguin.go Normal file
View File

@ -0,0 +1,114 @@
package entity
import (
"log"
"math"
"git.wisellama.rocks/Project-Ely/project-ely/internal/animation"
"git.wisellama.rocks/Project-Ely/project-ely/internal/physics"
rl "github.com/gen2brain/raylib-go/raylib"
)
type penguin struct {
animation AnimationTracker
object physics.Object
direction int
staticAnimation bool
color rl.Color
}
func NewPenguin() *penguin {
object := physics.NewObject()
anim := animation.NewAnimationTracker()
anim.SetAnimation(animation.PenguinDefault)
return &penguin{
animation: anim,
object: object,
staticAnimation: false,
color: rl.White,
}
}
func (p *penguin) Draw() error {
position := p.object.GetPosition()
// TODO?
//windowPosition := worldPosToWindowPos()
windowPosition := rl.Vector2{
X: float32(math.Round(float64(position.X))),
Y: float32(-1 * math.Round(float64(position.Y))),
}
return p.animation.Draw(windowPosition, p.color)
}
func (p *penguin) Update() error {
p.SetMoveAnimation()
return p.object.Update()
}
func (p *penguin) GetObject() physics.Object {
return p.object
}
func (p *penguin) GetAnimationTracker() AnimationTracker {
return p.animation
}
// ToggleStaticAnimation will set whether or not this entity should ever update its animation loop or not.
// True means this will never update, it is static.
// False means this will update the animation loop based on physical properties of this entity.
func (p *penguin) ToggleStaticAnimation() {
p.staticAnimation = !p.staticAnimation
}
func (p *penguin) SetMoveAnimation() {
if p.staticAnimation {
return
}
velocity := p.object.GetVelocity()
if velocity.X == 0 && velocity.Y == 0 {
// Stay facing whatever direction we were facing
direction := p.direction
switch direction {
case DirLeft:
p.animation.SetAnimation(animation.PenguinStationaryLeft)
case DirRight:
p.animation.SetAnimation(animation.PenguinStationaryRight)
case DirUp:
p.animation.SetAnimation(animation.PenguinStationaryUp)
case DirDown:
p.animation.SetAnimation(animation.PenguinStationaryDown)
default:
log.Printf("unknown direction: %v", direction)
p.animation.SetAnimation(animation.PenguinDefault)
}
} else {
// Figure out which way we are facing now that we're moving
direction := determineClosestDirection(velocity)
p.direction = direction
switch direction {
case DirLeft:
p.animation.SetAnimation(animation.PenguinWalkLeft)
case DirRight:
p.animation.SetAnimation(animation.PenguinWalkRight)
case DirUp:
p.animation.SetAnimation(animation.PenguinWalkUp)
case DirDown:
p.animation.SetAnimation(animation.PenguinWalkDown)
default:
log.Printf("unknown direction: %v", direction)
p.animation.SetAnimation(animation.PenguinDefault)
}
}
}
func (p *penguin) SetColor(color rl.Color) {
p.color = color
}

146
internal/game.go Normal file
View File

@ -0,0 +1,146 @@
package internal
import (
"context"
"fmt"
"log"
"math/rand"
"git.wisellama.rocks/Project-Ely/project-ely/internal/animation"
"git.wisellama.rocks/Project-Ely/project-ely/internal/channels"
"git.wisellama.rocks/Project-Ely/project-ely/internal/entity"
"git.wisellama.rocks/Project-Ely/project-ely/internal/player"
"git.wisellama.rocks/Project-Ely/project-ely/internal/sprite"
"git.wisellama.rocks/Project-Ely/project-ely/internal/version"
"git.wisellama.rocks/Wisellama/gosimpleconf"
rl "github.com/gen2brain/raylib-go/raylib"
)
// Drawable can be rendered with OpenGL. See the 'entity' and 'animation' packages.
type Drawable interface {
Draw() error
}
// Object can have physical interactions. See the 'entity' and 'physics' packages.
type Object interface {
Update() error
}
// Run is the main function to start the game.
func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error {
var err error
ctx, cancel := context.WithCancel(ctx)
defer cancel()
framerate64 := gosimpleconf.Int64(configMap["game.framerate"])
framerate := int32(framerate64)
windowTitle := fmt.Sprintf("%s - %s (%s)",
configMap["game.title"],
version.Version,
version.CommitHash,
)
// Initialize the RayLib window
channels.RL.Do(func() {
monitor := rl.GetCurrentMonitor()
windowWidth := int32(rl.GetMonitorWidth(monitor))
windowHeight := int32(rl.GetMonitorHeight(monitor))
rl.InitWindow(windowWidth, windowHeight, windowTitle)
rl.ToggleFullscreen()
rl.SetTargetFPS(framerate)
})
defer func() {
channels.RL.Do(func() {
rl.CloseWindow()
})
}()
// Initialize our sprites and animations
sprite.InitSpriteCache()
animation.DefineAnimations()
drawables := make([]Drawable, 0)
objects := make([]Object, 0)
center := rl.Vector2{X: float32(rl.GetScreenWidth()) / 2, Y: float32(rl.GetScreenHeight()) / 2 * -1}
player1 := player.NewPlayer(ctx)
player1Penguin := entity.NewPenguin()
player1Penguin.SetColor(rl.Gold)
player1Penguin.GetObject().SetPosition(center)
player1.SetControlledObject(player1Penguin.GetObject())
drawables = append(drawables, player1Penguin)
objects = append(objects, player1Penguin)
for i := 0; i < 10; i++ {
p := entity.NewPenguin()
p.GetObject().SetPosition(rl.Vector2{
X: rand.Float32() * float32(rl.GetScreenWidth()),
Y: rand.Float32() * float32(rl.GetScreenHeight()*-1),
})
p.GetAnimationTracker().SetAnimation(animation.RandomPenguinAnimation())
p.ToggleStaticAnimation()
drawables = append(drawables, p)
objects = append(objects, p)
}
// And now starting the main loop
running := true
for running {
channels.RL.Do(func() {
if rl.WindowShouldClose() {
cancel()
}
})
// Allow us to exit early if the context is done
select {
case <-ctx.Done():
running = false
default:
// Keep running
}
if !running {
break
}
player1.Update()
// Update physics
// TODO probably move out to something else to handle collisions.
for _, o := range objects {
err = o.Update()
if err != nil {
log.Printf("error updating physics: %v", err)
}
}
channels.RL.Do(func() {
rl.BeginDrawing()
})
// Draw stuff
for _, d := range drawables {
err = d.Draw()
if err != nil {
log.Printf("error drawing: %v", err)
}
}
channels.RL.Do(func() {
rl.ClearBackground(rl.Black)
rl.DrawText("Some Text!", 190, 200, 20, rl.Blue)
rl.DrawText(fmt.Sprintf("%v FPS", rl.GetFPS()), 190, 250, 20, rl.Blue)
})
channels.RL.Do(func() {
rl.EndDrawing()
})
}
sprite.CleanupSpriteCache()
return nil
}

View File

@ -1,15 +0,0 @@
package game
import "github.com/veandco/go-sdl2/sdl"
// camera represents a view of the world. It's a projection through the window looking at the world.
// Since this is only a 2D game with SDL, the projection is relatively simple: window + camera = world.
// https://gamedev.stackexchange.com/a/123844
type camera struct {
pos *sdl.Point
}
func NewCamera() *camera {
c := camera{}
return &c
}

View File

@ -1,53 +0,0 @@
package animation
import (
"github.com/veandco/go-sdl2/sdl"
)
type SpriteAnimation interface {
Draw(frame int, worldPosition *sdl.Point, angle float64, center *sdl.Point, flip sdl.RendererFlip) error
}
// An entityAnimation will take a SpriteAnimation and may manipulate it somehow (e.g. flipped for walking the other direction)
// This way you may cut down the actual number of pictures needed to define all the different animations.
type entityAnimation struct {
spriteAnimation SpriteAnimation
speed int
length int
angle float64
center *sdl.Point
flip sdl.RendererFlip
}
func NewEntityAnimation(
spriteAnimation SpriteAnimation,
speed int,
length int,
angle float64,
center *sdl.Point,
flip sdl.RendererFlip,
) *entityAnimation {
e := entityAnimation{
spriteAnimation: spriteAnimation,
speed: speed,
length: length,
angle: angle,
center: center,
flip: flip,
}
return &e
}
func (e *entityAnimation) Draw(frame int, windowPosition *sdl.Point) error {
return e.spriteAnimation.Draw(frame, windowPosition, e.angle, e.center, e.flip)
}
func (e *entityAnimation) GetSpeed() int {
return e.speed
}
func DefineAnimations() {
DefinePenguinAnimations()
}

View File

@ -1,80 +0,0 @@
package animation
import (
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/sprite"
"github.com/veandco/go-sdl2/sdl"
)
var PenguinAnimations map[int]*entityAnimation
const (
PENGUIN_WALK_RIGHT int = iota
PENGUIN_WALK_LEFT
PENGUIN_WALK_UP
PENGUIN_WALK_DOWN
PENGUIN_STATIONARY_RIGHT
PENGUIN_STATIONARY_LEFT
PENGUIN_STATIONARY_UP
PENGUIN_STATIONARY_DOWN
)
const (
PENGUIN_NUM_ANIMS = 8
PENGUIN_DEFAULT = PENGUIN_STATIONARY_RIGHT
)
func DefinePenguinAnimations() {
filename := sprite.PERCYWALKING
var (
dimensions sdl.Point
offset sdl.Point
center *sdl.Point
length int
speed int
border int
)
dimensions = sdl.Point{X: 13, Y: 17}
PenguinAnimations = make(map[int]*entityAnimation)
// Walking Right is in the spritesheet.
speed = 5
offset = sdl.Point{X: 0, Y: 1}
length = 5
border = 1 // optional border around each sprite
center = nil // center is for rotation, nil will default to w/2 h/2
walkRight := sprite.NewAnimation(filename, dimensions, offset, length, border)
PenguinAnimations[PENGUIN_WALK_RIGHT] = NewEntityAnimation(walkRight, speed, length, 0, center, sdl.FLIP_NONE)
// Walking Left is just that flipped.
PenguinAnimations[PENGUIN_WALK_LEFT] = NewEntityAnimation(walkRight, speed, length, 0, center, sdl.FLIP_HORIZONTAL)
// Stationary Right/Left is just the first frame.
length = 1
stationaryRight := sprite.NewAnimation(filename, dimensions, offset, length, border)
PenguinAnimations[PENGUIN_STATIONARY_RIGHT] = NewEntityAnimation(stationaryRight, speed, length, 0, center, sdl.FLIP_NONE)
PenguinAnimations[PENGUIN_STATIONARY_LEFT] = NewEntityAnimation(stationaryRight, speed, length, 0, center, sdl.FLIP_HORIZONTAL)
// Walk Up
length = 4
offset = sdl.Point{X: 0, Y: 3}
walkUp := sprite.NewAnimation(filename, dimensions, offset, length, border)
PenguinAnimations[PENGUIN_WALK_UP] = NewEntityAnimation(walkUp, speed, length, 0, center, sdl.FLIP_NONE)
// Stationary Up
length = 1
stationaryUp := sprite.NewAnimation(filename, dimensions, offset, length, border)
PenguinAnimations[PENGUIN_STATIONARY_UP] = NewEntityAnimation(stationaryUp, speed, length, 0, center, sdl.FLIP_NONE)
// Walk Down
length = 4
offset = sdl.Point{X: 0, Y: 0}
walkDown := sprite.NewAnimation(filename, dimensions, offset, length, border)
PenguinAnimations[PENGUIN_WALK_DOWN] = NewEntityAnimation(walkDown, speed, length, 0, center, sdl.FLIP_NONE)
// Stationary Down
length = 1
stationaryDown := sprite.NewAnimation(filename, dimensions, offset, length, border)
PenguinAnimations[PENGUIN_STATIONARY_DOWN] = NewEntityAnimation(stationaryDown, speed, length, 0, center, sdl.FLIP_NONE)
}

View File

@ -1,22 +0,0 @@
package command
// List of keys supported by all entities
const (
MOVE_X int = iota
MOVE_Y
SET_SPEED
SET_POSITION
SET_ANIMATION
)
type Command struct {
key int
value float64
}
func NewCommand(key int, value float64) Command {
return Command{
key: key,
value: value,
}
}

View File

@ -1,170 +0,0 @@
package command
import (
"context"
"log"
"sync"
"time"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/channels"
)
type Entity interface {
Draw() error
Update() error
MoveX(x float64)
MoveY(y float64)
SetSpeed(s float64)
}
type CommandHandler struct {
ctx context.Context
timeout time.Duration
entity Entity
commandChan chan Command
drawRequestChan chan bool
drawResponseChan chan bool
updateRequestChan chan bool
updateResponseChan chan bool
}
func NewCommandHandler(ctx context.Context, entity Entity) *CommandHandler {
commandChan := make(chan Command, 10)
drawRequestChan := make(chan bool)
drawResponseChan := make(chan bool)
updateRequestChan := make(chan bool)
updateResponseChan := make(chan bool)
defaultTimeout := time.Second
return &CommandHandler{
ctx: ctx,
timeout: defaultTimeout,
entity: entity,
commandChan: commandChan,
drawRequestChan: drawRequestChan,
drawResponseChan: drawResponseChan,
updateRequestChan: updateRequestChan,
updateResponseChan: updateResponseChan,
}
}
func (c *CommandHandler) closeResponses() {
close(c.drawResponseChan)
close(c.updateResponseChan)
}
func (c *CommandHandler) CloseRequests() {
close(c.drawRequestChan)
close(c.updateRequestChan)
close(c.commandChan)
}
func (c *CommandHandler) CommandRequest() chan Command {
return c.commandChan
}
func (c *CommandHandler) DrawRequest() chan bool {
return c.drawRequestChan
}
func (c *CommandHandler) DrawResponse() chan bool {
return c.drawResponseChan
}
func (c *CommandHandler) UpdateRequest() chan bool {
return c.updateRequestChan
}
func (c *CommandHandler) UpdateResponse() chan bool {
return c.updateResponseChan
}
func (c *CommandHandler) Run() {
defer c.closeResponses()
wg := sync.WaitGroup{}
running := true
for running {
select {
case <-c.drawRequestChan:
wg.Add(1)
go func() {
defer wg.Done()
c.DrawWithTimeout()
c.drawResponseChan <- true
}()
case <-c.updateRequestChan:
wg.Add(1)
go func() {
defer wg.Done()
c.UpdateWithTimeout()
c.updateResponseChan <- true
}()
case cmd := <-c.commandChan:
wg.Add(1)
go func(cmd Command) {
defer wg.Done()
c.HandleWithTimeout(cmd)
}(cmd)
case <-c.ctx.Done():
// Graceful shutdown
running = false
}
}
wg.Wait()
// Finish up anything in the queues
for cmd := range c.commandChan {
c.HandleWithTimeout(cmd)
}
for range c.drawRequestChan {
c.UpdateWithTimeout()
}
for range c.drawRequestChan {
c.DrawWithTimeout()
}
}
func (c *CommandHandler) HandleWithTimeout(cmd Command) {
err := channels.RunWithTimeout(c.timeout, func() error {
return c.Handle(cmd)
})
if err != nil {
log.Printf("handle with timeout error: %v\n", err)
}
}
func (c *CommandHandler) Handle(cmd Command) error {
switch cmd.key {
case MOVE_X:
c.entity.MoveX(cmd.value)
case MOVE_Y:
c.entity.MoveY(cmd.value)
case SET_SPEED:
c.entity.SetSpeed(cmd.value)
default:
log.Printf("unknown entity command: %v", cmd.key)
}
return nil
}
func (c *CommandHandler) DrawWithTimeout() {
err := channels.RunWithTimeout(c.timeout, func() error {
return c.entity.Draw()
})
if err != nil {
log.Printf("draw with timeout error: %v\n", err)
}
}
func (c *CommandHandler) UpdateWithTimeout() {
err := channels.RunWithTimeout(c.timeout, func() error {
return c.entity.Update()
})
if err != nil {
log.Printf("update with timeout error: %v\n", err)
}
}

View File

@ -1,52 +0,0 @@
package types
import (
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/vector"
"github.com/veandco/go-sdl2/sdl"
)
type EntityAnimation interface {
Draw(step int, windowPosition *sdl.Point) error
GetSpeed() int
}
const (
// These line up with the VEC_DIRECTIONS slice
DIR_LEFT int = iota
DIR_RIGHT
DIR_UP
DIR_DOWN
)
// The following are axis direction vectors based on world coordinates.
// UP/DOWN is intentionally UP = positive (which is different from window coordinates)
var (
VEC_LEFT = &vector.Vec2F{X: -1, Y: 0}
VEC_RIGHT = &vector.Vec2F{X: 1, Y: 0}
VEC_UP = &vector.Vec2F{X: 0, Y: 1}
VEC_DOWN = &vector.Vec2F{X: 0, Y: -1}
VEC_DIRECTIONS = []*vector.Vec2F{
// Prefer left/right animations by checking them first in the list
VEC_LEFT,
VEC_RIGHT,
VEC_UP,
VEC_DOWN,
}
)
func determineClosestDirection(velocity *vector.Vec2F) int {
closest := DIR_RIGHT
max := 0.0
buffer := 0.5 // This buffer lets us stick to the left/right animations for diagonal movement
for i, dir := range VEC_DIRECTIONS {
dot := velocity.Dot(dir)
if dot > max+buffer {
max = dot
closest = i
}
}
return closest
}

View File

@ -1,166 +0,0 @@
package types
import (
"log"
"math"
"sync"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/animation"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/vector"
"github.com/veandco/go-sdl2/sdl"
)
type penguin struct {
mx sync.RWMutex
// Animation parameters
currentAnimation EntityAnimation // pointer to current animation loop
animationStep int // index of animation loop
direction int // direction facing for sprite animations
updateAnimation bool // if false, don't change the animation
// Physical parameters
worldPosition *vector.Vec2F // where is the center of this object
velocity *vector.Vec2F // movement direction to be applied each tick
speed float64 // movement magnitude to multiply with the velocity
}
func NewPenguin(renderer *sdl.Renderer) *penguin {
position := vector.Vec2F{}
velocity := vector.Vec2F{}
speed := 1.0
p := penguin{
currentAnimation: animation.PenguinAnimations[animation.PENGUIN_DEFAULT],
animationStep: 0,
direction: DIR_RIGHT,
worldPosition: &position,
speed: speed,
velocity: &velocity,
}
return &p
}
func (p *penguin) Draw() error {
step := p.animationStep / p.currentAnimation.GetSpeed()
// TODO
//windowPosition := worldPosToWindowPos()
windowPosition := &sdl.Point{
X: int32(math.Round(p.worldPosition.X)),
Y: int32(math.Round(-1 * p.worldPosition.Y)),
}
err := p.currentAnimation.Draw(step, windowPosition)
if err != nil {
return err
}
p.animationStep = 1 + p.animationStep
return nil
}
func (p *penguin) SetPosition(vec *vector.Vec2F) {
p.worldPosition = vec
}
func (p *penguin) SetDirection(dir int) {
p.direction = dir
}
func (p *penguin) SetAnimation(id int) {
a, exists := animation.PenguinAnimations[id]
if !exists {
log.Printf("animation does not exist: %v", id)
a = animation.PenguinAnimations[animation.PENGUIN_DEFAULT]
}
if a != p.currentAnimation {
p.animationStep = 0
}
p.currentAnimation = a
}
func (p *penguin) MoveX(x float64) {
p.mx.Lock()
defer p.mx.Unlock()
p.velocity.X = x
p.velocity.Normalize()
p.updateAnimation = true
}
func (p *penguin) MoveY(y float64) {
p.mx.Lock()
defer p.mx.Unlock()
// (0,0) is the top left, so negative y moves up
p.velocity.Y = y
p.velocity.Normalize()
p.updateAnimation = true
}
func (p *penguin) SetSpeed(s float64) {
p.speed = s
}
func (p *penguin) GetSpeed() float64 {
return p.speed
}
func (p *penguin) SetMoveAnimation() {
if p.velocity.Zero() {
// Stay facing whatever direction we were facing
switch p.direction {
case DIR_LEFT:
p.SetAnimation(animation.PENGUIN_STATIONARY_LEFT)
case DIR_RIGHT:
p.SetAnimation(animation.PENGUIN_STATIONARY_RIGHT)
case DIR_UP:
p.SetAnimation(animation.PENGUIN_STATIONARY_UP)
case DIR_DOWN:
p.SetAnimation(animation.PENGUIN_STATIONARY_DOWN)
default:
log.Printf("unknown direction: %v", p.direction)
p.SetAnimation(animation.PENGUIN_DEFAULT)
}
} else {
// Figure out which way we are facing now that we're moving
p.direction = determineClosestDirection(p.velocity)
switch p.direction {
case DIR_LEFT:
p.SetAnimation(animation.PENGUIN_WALK_LEFT)
case DIR_RIGHT:
p.SetAnimation(animation.PENGUIN_WALK_RIGHT)
case DIR_UP:
p.SetAnimation(animation.PENGUIN_WALK_UP)
case DIR_DOWN:
p.SetAnimation(animation.PENGUIN_WALK_DOWN)
default:
log.Printf("unknown direction: %v", p.direction)
p.SetAnimation(animation.PENGUIN_DEFAULT)
}
}
}
func (p *penguin) Update() error {
if p.updateAnimation {
p.SetMoveAnimation()
p.updateAnimation = false
}
x := p.velocity.X * p.speed
y := p.velocity.Y * p.speed
p.worldPosition.X += x
p.worldPosition.Y += y
return nil
}

View File

@ -1,210 +0,0 @@
package game
import (
"context"
"fmt"
"log"
"math/rand"
"strconv"
"sync"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/animation"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/command"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/types"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/player"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/sprite"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/window"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/vector"
"gitea.wisellama.rocks/Wisellama/gosimpleconf"
"github.com/veandco/go-sdl2/sdl"
)
type EntityCmdHandler interface {
Run()
CloseRequests()
CommandRequest() chan command.Command
DrawRequest() chan bool
DrawResponse() chan bool
UpdateRequest() chan bool
UpdateResponse() chan bool
}
// Run is the main function to start the game.
func Run(ctx context.Context, configMap gosimpleconf.ConfigMap) error {
var err error
ctx, cancel := context.WithCancel(ctx)
defer cancel()
framerate, err := strconv.ParseFloat(configMap["game.framerate"], 64)
if err != nil {
err = fmt.Errorf("error parsing framerate: %w", err)
return err
}
framerateDelay := uint32(1000 / framerate)
err = window.SdlInit()
if err != nil {
return err
}
defer window.SdlQuit()
gameWindow, err := window.NewWindow(configMap["game.title"])
if err != nil {
err = fmt.Errorf("failed creating GameWindow: %w", err)
return err
}
defer gameWindow.Cleanup()
var renderer *sdl.Renderer
sdl.Do(func() {
renderer, err = sdl.CreateRenderer(gameWindow.SdlWindow, -1, sdl.RENDERER_ACCELERATED)
})
if err != nil {
err = fmt.Errorf("failed creating SDL renderer: %w", err)
return err
}
defer func() {
sdl.Do(func() {
err = renderer.Destroy()
if err != nil {
log.Printf("error destroying renderer: %v\n", err)
}
})
}()
err = sprite.InitSpriteCache(renderer)
if err != nil {
err = fmt.Errorf("failed in InitSpriteCache: %w", err)
return err
}
defer sprite.CleanupSpriteCache()
animation.DefineAnimations()
inputHandler := window.NewInputHandler(ctx)
// Done with main setup, now moving on to creating specific entities
entityList := make([]EntityCmdHandler, 0)
wg := sync.WaitGroup{}
// Setup Player 1
// Let them control a penguin to start with
player1 := player.NewPlayer(ctx, inputHandler)
penguinEntity := types.NewPenguin(renderer)
penguinCmdHandler := command.NewCommandHandler(ctx, penguinEntity)
penguinEntity.SetSpeed(2.0)
entityList = append(entityList, penguinCmdHandler)
player1.SetEntityChan(penguinCmdHandler.CommandRequest())
wg.Add(1)
go func() {
defer wg.Done()
penguinCmdHandler.Run()
}()
for i := 0; i < 10; i++ {
entity := types.NewPenguin(renderer)
entityCmd := command.NewCommandHandler(ctx, entity)
randomPos := vector.Vec2F{X: rand.Float64() * 500, Y: rand.Float64() * -500}
entity.SetPosition(&randomPos)
entity.SetAnimation(rand.Intn(animation.PENGUIN_NUM_ANIMS))
entityList = append(entityList, entityCmd)
wg.Add(1)
go func() {
defer wg.Done()
entityCmd.Run()
}()
}
// Start the inputHandler
wg.Add(1)
go func() {
defer wg.Done()
inputHandler.Run()
}()
// And now starting the main loop
running := true
for running {
// Poll for SDL events
var event sdl.Event
sdl.Do(func() {
event = sdl.PollEvent()
for event != nil {
switch e := event.(type) {
case *sdl.QuitEvent:
log.Println("QuitEvent quitting")
cancel()
case *sdl.KeyboardEvent:
if e.Keysym.Sym == sdl.K_ESCAPE {
log.Println("Esc quitting")
cancel()
} else {
// Publish the event to the inputHandler
inputHandler.KeyboardChan() <- *e
}
}
event = sdl.PollEvent()
}
})
// Allow us to exit early if the context is done
select {
case <-ctx.Done():
running = false
default:
// Keep running
}
if !running {
break
}
// Players
player1.Update()
// Background
sdl.Do(func() {
err = renderer.SetDrawColor(0, 120, 0, 255)
if err != nil {
log.Printf("error in renderer.SetDrawColor: %v\n", err)
}
err = renderer.Clear()
if err != nil {
log.Printf("error in renderer.Clear: %v\n", err)
}
})
// Tell everything to Update and Draw
for _, e := range entityList {
e.UpdateRequest() <- true
e.DrawRequest() <- true
}
// Wait for each entity to finish their Draw and Update commands before proceeding
for _, e := range entityList {
<-e.UpdateResponse()
<-e.DrawResponse()
}
// Draw
sdl.Do(func() {
renderer.Present()
sdl.Delay(framerateDelay)
})
}
inputHandler.CloseChannels()
for _, e := range entityList {
e.CloseRequests()
}
wg.Wait()
return nil
}

View File

@ -1,68 +0,0 @@
package player
import (
"context"
"time"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game/entity/command"
"github.com/veandco/go-sdl2/sdl"
)
type InputHandler interface {
Keystate(keycode sdl.Keycode) bool
}
// A player represents a collection of stuff controlled by the user's input.
// It contains the camera used to view the world,
// the viewport for the part of the screen to draw to (splitscreen support),
// as well as the entity the player is currently controlling.
type player struct {
ctx context.Context
timeout time.Duration
inputHandler InputHandler
entityChan chan command.Command
}
func NewPlayer(ctx context.Context, inputHandler InputHandler) *player {
defaultTimeout := time.Second
p := player{
ctx: ctx,
timeout: defaultTimeout,
inputHandler: inputHandler,
}
return &p
}
func (p *player) SetEntityChan(e chan command.Command) {
p.entityChan = e
}
func (p *player) Update() error {
// Speed
if p.inputHandler.Keystate(sdl.K_LSHIFT) {
p.entityChan <- command.NewCommand(command.SET_SPEED, 4)
} else {
p.entityChan <- command.NewCommand(command.SET_SPEED, 2)
}
// Move X
if p.inputHandler.Keystate(sdl.K_d) {
p.entityChan <- command.NewCommand(command.MOVE_X, 1.0)
} else if p.inputHandler.Keystate(sdl.K_a) {
p.entityChan <- command.NewCommand(command.MOVE_X, -1.0)
} else if !p.inputHandler.Keystate(sdl.K_d) && !p.inputHandler.Keystate(sdl.K_a) {
p.entityChan <- command.NewCommand(command.MOVE_X, 0.0)
}
// Move Y
if p.inputHandler.Keystate(sdl.K_w) {
p.entityChan <- command.NewCommand(command.MOVE_Y, 1.0)
} else if p.inputHandler.Keystate(sdl.K_s) {
p.entityChan <- command.NewCommand(command.MOVE_Y, -1.0)
} else if !p.inputHandler.Keystate(sdl.K_w) && !p.inputHandler.Keystate(sdl.K_s) {
p.entityChan <- command.NewCommand(command.MOVE_Y, 0.0)
}
return nil
}

View File

@ -1,99 +0,0 @@
package sprite
import (
"fmt"
"log"
"github.com/veandco/go-sdl2/img"
"github.com/veandco/go-sdl2/sdl"
)
type spritesheet struct {
filename string
renderer *sdl.Renderer
surface *sdl.Surface // the original png file
texture *sdl.Texture // the SDL texture created from the image
}
func NewSprite(renderer *sdl.Renderer, filename string) (*spritesheet, error) {
var err error
// Load the surface file
var surface *sdl.Surface
sdl.Do(func() {
surface, err = img.Load(filename)
})
if err != nil {
err = fmt.Errorf("failed to load image: %w", err)
return nil, err
}
// Create the sprite sheet texture from the image
var texture *sdl.Texture
sdl.Do(func() {
texture, err = renderer.CreateTextureFromSurface(surface)
})
if err != nil {
err = fmt.Errorf("failed to create texture: %w", err)
return nil, err
}
s := spritesheet{
filename: filename,
renderer: renderer,
surface: surface,
texture: texture,
}
return &s, nil
}
func (s *spritesheet) Cleanup() {
// Clean up image
defer func() {
if s.surface != nil {
sdl.Do(func() {
s.surface.Free()
})
}
}()
// Clean up spritesheet
defer func() {
if s.texture != nil {
sdl.Do(func() {
err := s.texture.Destroy()
if err != nil {
log.Printf("error destroying spritesheet %v: %v\n", s.filename, err)
}
})
}
}()
}
func (s *spritesheet) Draw(
section *sdl.Rect,
placement *sdl.Rect,
angle float64,
center *sdl.Point,
flip sdl.RendererFlip,
) error {
var err error
sdl.Do(func() {
err = s.renderer.CopyEx(s.texture, section, placement, angle, center, flip)
})
if err != nil {
return err
}
return nil
}
func (s *spritesheet) Bounds() *sdl.Point {
p := sdl.Point{
X: s.surface.W,
Y: s.surface.H,
}
return &p
}

View File

@ -1,87 +0,0 @@
package sprite
import (
"log"
"github.com/veandco/go-sdl2/sdl"
)
const (
PENGUIN = "assets/images/penguin.png"
PERCYWALKING = "assets/images/percywalking.png"
PLATFORMER_FOREST_CHARACTERS = "assets/images/a-platformer-in-the-forest/characters.png"
)
var fileList []string = []string{
PENGUIN,
PERCYWALKING,
PLATFORMER_FOREST_CHARACTERS,
}
var (
spriteCache map[string]*spritesheet
defaultSprite *spritesheet
)
func InitSpriteCache(renderer *sdl.Renderer) error {
var err error
defaultSprite, err = createDefaultSprite(renderer)
if err != nil {
log.Printf("failed to create DefaultSprite: %v", err)
return err
}
spriteCache = make(map[string]*spritesheet)
for _, filename := range fileList {
s, err := NewSprite(renderer, filename)
if err != nil {
log.Printf("error creating sprite %v, using DefaultSprite: %v", filename, err)
spriteCache[filename] = defaultSprite
} else {
spriteCache[filename] = s
}
}
return nil
}
func GetSpritesheet(filename string) *spritesheet {
s, exists := spriteCache[filename]
if !exists {
log.Printf("no sprite found for %v, using DefaultSprite", filename)
return defaultSprite
}
return s
}
func CleanupSpriteCache() {
for _, v := range spriteCache {
defer v.Cleanup()
}
}
// The DefaultSprite is just a 100x100 black square
func createDefaultSprite(renderer *sdl.Renderer) (*spritesheet, error) {
var err error
surface, err := sdl.CreateRGBSurface(0, 100, 100, 32, 0, 0, 0, 0)
if err != nil {
return nil, err
}
texture, err := renderer.CreateTextureFromSurface(surface)
if err != nil {
return nil, err
}
s := spritesheet{
renderer: renderer,
texture: texture,
surface: surface,
}
return &s, nil
}

View File

@ -1,75 +0,0 @@
package window
import (
"context"
"sync"
"github.com/veandco/go-sdl2/sdl"
)
type inputHandler struct {
ctx context.Context
keyboardChan chan sdl.KeyboardEvent
// The actual internal state
mxKeyboard sync.RWMutex
keystates map[sdl.Keycode]bool
// TODO joystick/gamepad
// mxJoystick sync.RWMutex
}
func NewInputHandler(ctx context.Context) *inputHandler {
keyboardChan := make(chan sdl.KeyboardEvent)
keystates := make(map[sdl.Keycode]bool)
i := inputHandler{
ctx: ctx,
keystates: keystates,
keyboardChan: keyboardChan,
}
return &i
}
func (i *inputHandler) CloseChannels() {
close(i.keyboardChan)
}
func (i *inputHandler) KeyboardChan() chan sdl.KeyboardEvent {
return i.keyboardChan
}
func (i *inputHandler) Run() {
running := true
for running {
select {
case event := <-i.keyboardChan:
i.UpdateKeyboard(event)
case <-i.ctx.Done():
running = false
}
}
// Finish up anything in the queues
for e := range i.keyboardChan {
i.UpdateKeyboard(e)
}
}
func (i *inputHandler) UpdateKeyboard(e sdl.KeyboardEvent) {
i.mxKeyboard.Lock()
defer i.mxKeyboard.Unlock()
// Key states (just set a boolean whether the key is actively being pressed)
if e.Type == sdl.KEYDOWN {
i.keystates[e.Keysym.Sym] = true
} else if e.Type == sdl.KEYUP {
i.keystates[e.Keysym.Sym] = false
}
}
func (i *inputHandler) Keystate(keycode sdl.Keycode) bool {
i.mxKeyboard.RLock()
defer i.mxKeyboard.RUnlock()
return i.keystates[keycode]
}

View File

@ -1,60 +0,0 @@
package window
import (
"fmt"
"github.com/veandco/go-sdl2/sdl"
)
/*
NOTE: We're using the SDL with goroutines example.
Every call to an SDL function must be passed through the sdl.Do() function so that it joins the queue of thread-sensitive calls.
You'll see this a lot below, but basically every SDL function must do something like this:
sdl.Do(func() {
err = sdl.SomeFunction()
})
This is for thread safety because SDL is not intended to be thread-safe.
However, the rest of the code can do whatever it wants with threads so long as the SDL calls all use this method.
*/
const (
SDL_INIT_FLAGS uint32 = sdl.INIT_TIMER | sdl.INIT_AUDIO | sdl.INIT_VIDEO | sdl.INIT_EVENTS | sdl.INIT_JOYSTICK | sdl.INIT_HAPTIC | sdl.INIT_GAMECONTROLLER // ignore sensor subsystem
SDL_WINDOW_FLAGS uint32 = sdl.WINDOW_SHOWN | sdl.WINDOW_RESIZABLE
SDL_FULLSCREEN_WINDOW_FLAGS uint32 = SDL_WINDOW_FLAGS | sdl.WINDOW_FULLSCREEN_DESKTOP
SDL_WINDOW_WIDTH int32 = 800
SDL_WINDOW_HEIGHT int32 = 600
)
func SdlInit() error {
var err error
// Initialize SDL
sdl.Do(func() {
err = sdl.Init(SDL_INIT_FLAGS)
})
if err != nil {
err = fmt.Errorf("failed initializing SDL: %w", err)
return err
}
// Set some base SDL settings
sdl.Do(func() {
// Disable the Linux compositor flicker.
// https://github.com/mosra/magnum/issues/184#issuecomment-425952900
sdl.SetHint(sdl.HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")
sdl.DisableScreenSaver()
// Capture the mouse for movement
//sdl.SetRelativeMouseMode(true)
})
return nil
}
func SdlQuit() {
sdl.Do(func() {
sdl.Quit()
})
}

View File

@ -1,51 +0,0 @@
package window
import (
"log"
"github.com/veandco/go-sdl2/sdl"
)
type window struct {
SdlWindow *sdl.Window
KeyStates map[sdl.Keycode]bool
}
func NewWindow(title string) (*window, error) {
var (
err error
sdlWindow *sdl.Window
)
sdl.Do(func() {
sdlWindow, err = sdl.CreateWindow(
title,
sdl.WINDOWPOS_UNDEFINED,
sdl.WINDOWPOS_UNDEFINED,
SDL_WINDOW_WIDTH,
SDL_WINDOW_HEIGHT,
SDL_WINDOW_FLAGS)
})
if err != nil {
return nil, err
}
keyStates := make(map[sdl.Keycode]bool)
gw := &window{
SdlWindow: sdlWindow,
KeyStates: keyStates,
}
return gw, nil
}
func (g *window) Cleanup() {
if g.SdlWindow != nil {
sdl.Do(func() {
err := g.SdlWindow.Destroy()
if err != nil {
log.Printf("error destroying SdlWindow: %v\n", err)
}
})
}
}

View File

@ -0,0 +1,87 @@
package physics
import rl "github.com/gen2brain/raylib-go/raylib"
// Object defines the interface for physical objects.
//
// This is defined here in the object.go file because after
// passing an 'object' to an 'entity' (e.g. 'penguin'),
// then to a 'player', as well as the main function,
// we needed almost all of the functions for the whole
// interface defined in each place.
type Object interface {
Update() error
GetPosition() rl.Vector2
GetVelocity() rl.Vector2
GetAcceleration() rl.Vector2
GetBaseAcceleration() rl.Vector2
SetPosition(pos rl.Vector2)
SetVelocity(vel rl.Vector2)
SetAcceleration(acc rl.Vector2)
SetBaseAcceleration(base rl.Vector2)
}
type object struct {
position rl.Vector2
velocity rl.Vector2
acceleration rl.Vector2
baseAcceleration rl.Vector2 // some default speed for this object
}
func NewObject() *object {
pos := rl.Vector2{X: 0.0, Y: 0.0}
vel := rl.Vector2{X: 0.0, Y: 0.0}
acc := rl.Vector2{X: 0.0, Y: 0.0}
base := rl.Vector2{X: 1.0, Y: 1.0}
return &object{
position: pos,
velocity: vel,
acceleration: acc,
baseAcceleration: base,
}
}
func (o *object) Update() error {
p := o.position
v := o.velocity
a := o.acceleration
v = rl.Vector2Add(v, a)
o.velocity = v
p = rl.Vector2Add(p, v)
o.position = p
return nil
}
func (o *object) SetPosition(pos rl.Vector2) {
o.position = pos
}
func (o *object) SetVelocity(vel rl.Vector2) {
o.velocity = vel
}
func (o *object) SetAcceleration(acc rl.Vector2) {
o.acceleration = acc
}
func (o *object) SetBaseAcceleration(base rl.Vector2) {
o.baseAcceleration = base
}
func (o *object) GetPosition() rl.Vector2 {
return o.position
}
func (o *object) GetVelocity() rl.Vector2 {
return o.velocity
}
func (o *object) GetAcceleration() rl.Vector2 {
return o.acceleration
}
func (o *object) GetBaseAcceleration() rl.Vector2 {
return o.baseAcceleration
}

84
internal/player/player.go Normal file
View File

@ -0,0 +1,84 @@
package player
import (
"context"
"time"
"git.wisellama.rocks/Project-Ely/project-ely/internal/channels"
rl "github.com/gen2brain/raylib-go/raylib"
)
type PhysicalObject interface {
SetVelocity(vel rl.Vector2)
}
// A player represents a collection of stuff controlled by the user's input.
// It contains the camera used to view the world,
// the viewport for the part of the screen to draw to (splitscreen support),
// as well as the entity the player is currently controlling.
type player struct {
controlledObject PhysicalObject
ctx context.Context
timeout time.Duration
}
func NewPlayer(ctx context.Context) *player {
defaultTimeout := time.Second
p := player{
ctx: ctx,
timeout: defaultTimeout,
}
return &p
}
func (p *player) SetControlledObject(c PhysicalObject) {
p.controlledObject = c
}
func (p *player) Update() {
var (
up bool
down bool
left bool
right bool
fast bool
)
channels.RL.Do(func() {
fast = rl.IsKeyDown(rl.KeyLeftShift)
left = rl.IsKeyDown(rl.KeyA) || rl.IsKeyDown(rl.KeyLeft)
right = rl.IsKeyDown(rl.KeyD) || rl.IsKeyDown(rl.KeyRight)
up = rl.IsKeyDown(rl.KeyW) || rl.IsKeyDown(rl.KeyUp)
down = rl.IsKeyDown(rl.KeyS) || rl.IsKeyDown(rl.KeyDown)
})
baseVel := rl.Vector2{X: 1.0, Y: 1.0}
velocity := rl.Vector2{X: 0.0, Y: 0.0}
// Move horizontal
if right {
velocity.X = baseVel.X
} else if left {
velocity.X = baseVel.X * -1
}
// Move vertical
if up {
velocity.Y = baseVel.Y
} else if down {
velocity.Y = baseVel.Y * -1
}
// If we're moving diagonal, normalize the vector to avoid weird diagonal super-speed
if velocity.X != 0 || velocity.Y != 0 {
velocity = rl.Vector2Normalize(velocity)
}
// Faster speed
if fast {
velocity.X *= 2
velocity.Y *= 2
}
p.controlledObject.SetVelocity(velocity)
}

17
internal/sprite/files.go Normal file
View File

@ -0,0 +1,17 @@
package sprite
const (
Penguin = "assets/images/penguin.png"
PercyWalking = "assets/images/percywalking.png"
PlatformerForestCharacters = "assets/images/a-platformer-in-the-forest/characters.png"
DelilahWalking = "assets/images/Delilah_walking.png"
KingWalking = "assets/images/King_walking.png"
)
var SpriteFileList []string = []string{
Penguin,
PercyWalking,
PlatformerForestCharacters,
DelilahWalking,
KingWalking,
}

View File

@ -3,80 +3,86 @@ package sprite
import (
"fmt"
"github.com/veandco/go-sdl2/sdl"
rl "github.com/gen2brain/raylib-go/raylib"
)
// spriteAnimation specifies which subsections of a spritesheet define this animation.
// For example, walking to the right could be defined by 4 subsections of a sprite sheet.
type spriteAnimation struct {
spritesheet *spritesheet
dimensions sdl.Point
offset sdl.Point
dimensions rl.Vector2
offset rl.Vector2
length int
border int
}
func NewAnimation(
filename string,
dimensions sdl.Point,
offset sdl.Point,
dimensions rl.Vector2,
offset rl.Vector2,
length int,
border int,
) *spriteAnimation {
) spriteAnimation {
spritesheet := GetSpritesheet(filename)
a := spriteAnimation{
return spriteAnimation{
spritesheet: spritesheet,
dimensions: dimensions,
offset: offset,
length: length,
border: border,
}
return &a
}
func (a *spriteAnimation) Draw(
func (a spriteAnimation) Draw(
frame int,
windowPosition *sdl.Point,
angle float64,
center *sdl.Point,
flip sdl.RendererFlip,
windowPosition rl.Vector2,
angle float32,
center rl.Vector2,
flip rl.Vector2,
color rl.Color,
) error {
width := a.dimensions.X
height := a.dimensions.Y
border := int32(a.border)
border := float32(a.border)
base := sdl.Point{
base := rl.Vector2{
X: (width+border)*a.offset.X + border,
Y: (height+border)*a.offset.Y + border,
}
// Assuming all frames are horizontal going left to right.
// Potentially with a border offset as well.
f := int32(frame % a.length)
section := sdl.Rect{
X: base.X + f*(width+border),
Y: base.Y,
W: width,
H: height,
f := float32(frame % a.length)
section := rl.Rectangle{
X: base.X + f*(width+border),
Y: base.Y,
Width: width,
Height: height,
}
err := a.checkBounds(&section)
if flip.X > 0 {
section.Width *= -1
}
if flip.Y > 0 {
section.Height *= -1
}
err := a.checkBounds(section)
if err != nil {
return err
}
placement := sdl.Rect{
X: windowPosition.X,
Y: windowPosition.Y,
W: width * 2, // TODO just testing with x2, eventually scale based on screen size or something
H: height * 2,
placement := rl.Rectangle{
X: windowPosition.X,
Y: windowPosition.Y,
Width: width * 2,
Height: height * 2,
}
err = a.spritesheet.Draw(&section, &placement, angle, center, flip)
err = a.spritesheet.Draw(section, placement, angle, center, color)
if err != nil {
return err
}
@ -84,9 +90,9 @@ func (a *spriteAnimation) Draw(
return nil
}
func (a *spriteAnimation) checkBounds(section *sdl.Rect) error {
width := a.spritesheet.surface.W
height := a.spritesheet.surface.H
func (a spriteAnimation) checkBounds(section rl.Rectangle) error {
width := float32(a.spritesheet.texture.Width)
height := float32(a.spritesheet.texture.Height)
outOfBounds := false
if section.X < 0 {
@ -95,15 +101,15 @@ func (a *spriteAnimation) checkBounds(section *sdl.Rect) error {
if section.Y < 0 {
outOfBounds = true
}
if section.X+section.W > width {
if section.X+section.Width > width {
outOfBounds = true
}
if section.Y+section.H > height {
if section.Y+section.Height > height {
outOfBounds = true
}
if outOfBounds {
return fmt.Errorf("draw section was out of bounds - section: %v, image: %v", *section, a.spritesheet.surface.Bounds())
return fmt.Errorf("draw section was out of bounds - section: %v, image: %v", section, a.spritesheet.Bounds())
}
return nil

View File

@ -0,0 +1,55 @@
package sprite
import (
"git.wisellama.rocks/Project-Ely/project-ely/internal/channels"
rl "github.com/gen2brain/raylib-go/raylib"
)
type spritesheet struct {
filename string
texture rl.Texture2D
}
func NewSprite(filename string) *spritesheet {
var texture rl.Texture2D
channels.RL.Do(func() {
texture = rl.LoadTexture(filename)
})
s := spritesheet{
filename: filename,
texture: texture,
}
return &s
}
func (s *spritesheet) Cleanup() {
channels.RL.Do(func() {
// Clean up spritesheet
rl.UnloadTexture(s.texture)
})
}
func (s *spritesheet) Draw(
section rl.Rectangle,
placement rl.Rectangle,
angle float32,
center rl.Vector2,
color rl.Color,
) error {
channels.RL.Do(func() {
origin := rl.Vector2{}
rl.DrawTexturePro(s.texture, section, placement, origin, angle, color)
})
return nil
}
func (s *spritesheet) Bounds() rl.Vector2 {
return rl.Vector2{
X: float32(s.texture.Width),
Y: float32(s.texture.Height),
}
}

View File

@ -0,0 +1,33 @@
package sprite
import (
"log"
)
var (
spriteCache map[string]*spritesheet
)
func InitSpriteCache() {
spriteCache = make(map[string]*spritesheet)
for _, filename := range SpriteFileList {
s := NewSprite(filename)
spriteCache[filename] = s
}
}
func GetSpritesheet(filename string) *spritesheet {
s, exists := spriteCache[filename]
if !exists {
log.Printf("no sprite found for %v", filename)
}
return s
}
func CleanupSpriteCache() {
for _, v := range spriteCache {
defer v.Cleanup()
}
}

View File

@ -1,34 +0,0 @@
package vector
import "math"
// This is a small-scale vector implementation for a Vec2F.
// If you need anything more complicated than normals and dot-products,
// then you should probably just switch to the glmath library instead.
type Vec2F struct {
X float64
Y float64
}
func (v *Vec2F) Zero() bool {
return v.X == 0.0 && v.Y == 0.0
}
func (v *Vec2F) LengthSquared() float64 {
return v.X*v.X + v.Y*v.Y
}
func (v *Vec2F) Normalize() {
length := math.Hypot(v.X, v.Y)
if length == 0 {
return
}
v.X = v.X / length
v.Y = v.Y / length
}
func (v *Vec2F) Dot(other *Vec2F) float64 {
return v.X*other.X + v.Y*other.Y
}

View File

@ -0,0 +1,18 @@
package version
import "fmt"
// Thanks to the following blog post for how to use the linker flags
// to pass in build-time variables.
//
// https://belief-driven-design.com/build-time-variables-in-go-51439b26ef9/
var (
Version = "dev"
CommitHash = "n/a"
BuildTimestamp = "n/a"
)
func BuildVersion() string {
return fmt.Sprintf("%s %s %s", Version, CommitHash, BuildTimestamp)
}

93
main.go
View File

@ -1,44 +1,43 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/config"
"gitea.wisellama.rocks/Project-Ely/project-ely/internal/game"
"gitea.wisellama.rocks/Wisellama/gosimpleconf"
"github.com/veandco/go-sdl2/sdl"
"git.wisellama.rocks/Project-Ely/project-ely/internal"
"git.wisellama.rocks/Project-Ely/project-ely/internal/channels"
"git.wisellama.rocks/Wisellama/gopackagebase"
"git.wisellama.rocks/Wisellama/gosimpleconf"
)
const (
configFile = "project-ely.conf"
)
var defaultConfig gosimpleconf.ConfigMap = gosimpleconf.ConfigMap{
"game.title": "Project Ely",
"game.framerate": "60",
"log.utcTime": "false",
"log.writeToFile": "false",
}
func main() {
// Setup some initial context for gracefully killing
// the program with system interrupt signals like ctrl+c
// https://pace.dev/blog/2020/02/17/repond-to-ctrl-c-interrupt-signals-gracefully-with-context-in-golang-by-mat-ryer.html
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
defer func() {
signal.Stop(signalChan)
cancel()
}()
go func() {
select {
case <-signalChan:
// Graceful exit on ctrl+c
log.Printf("Attempting to exit gracefully...\n")
cancel()
case <-ctx.Done():
}
<-signalChan // Hard exit on second ctrl+c
log.Printf("Hard kill\n")
os.Exit(2)
}()
var err error
baseConfig, err := gopackagebase.Initialize(configFile, defaultConfig)
if err != nil {
log.Fatalf("error initializing: %v", err)
}
if baseConfig == nil {
log.Fatalf("baseConfig was nil")
}
defer baseConfig.Cancel()
// Run the program
err := run(ctx)
log.Printf("=== Starting %v ===", baseConfig.ConfigMap["game.title"])
channels.RL.Main(func() {
err = internal.Run(baseConfig.Ctx, baseConfig.ConfigMap)
})
if err != nil {
log.Printf("%v\n", err)
os.Exit(1)
@ -46,37 +45,3 @@ func main() {
log.Printf("Exited gracefully!\n")
}
func run(ctx context.Context) error {
var err error
// Read configuration
configMap, err := config.Configure("game.conf")
if err != nil {
log.Fatalf("error during configure: %v\n", err)
}
// Setup logging
writeToFile := gosimpleconf.Bool(configMap["log.writeToFile"])
logFilename := configMap["log.file"]
logWriter, err := config.SetupLogging(writeToFile, logFilename)
if err != nil {
log.Fatalf("error setting up logging: %v\n", err)
}
defer logWriter.Cleanup()
// Setup Random Number Generator seed
config.InitRNG()
// Start everything with the SDL goroutine context
log.Printf("=== Starting %v ===", configMap["game.title"])
sdl.Main(func() {
err = game.Run(ctx, configMap)
})
if err != nil {
return err
}
return nil
}

7
scripts/build-unix.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env sh
d=$(cd $(dirname $0) && pwd -P)
. "${d}/common-release-build.sh"
build $(uname) $(uname -m)

View File

@ -1,5 +1,7 @@
#!/usr/bin/env sh
. common-build-windows.sh
d=$(cd $(dirname $0) && pwd -P)
. "${d}/common-release-build.sh"
build windows 386

View File

@ -1,5 +0,0 @@
#!/usr/bin/env sh
. common-release-build.sh
build windows amd64

View File

@ -0,0 +1,7 @@
#!/usr/bin/env sh
d=$(cd $(dirname $0) && pwd -P)
. "${d}/common-release-build.sh"
build windows amd64

View File

@ -5,26 +5,38 @@ build(){
OS=$1
ARCH=$2
if [ -z $NAME ]
then
NAME=output
fi
# Lowercase
OS=$(echo "$OS" | awk '{ print tolower($0) }')
ARCH=$(echo "$ARCH" | awk '{ print tolower($0) }')
if [ -z "${CC}" ]
then
export CC=gcc
fi
LDFLAGS_WIN=""
# Add .exe to Windows output
EXE=""
if [ "${OS}" == "windows" ]
if [ "${OS}" = "windows" ]
then
# Hide the windows console popup for SDL
LDFLAGS_WIN="-H windowsgui"
EXE=".exe"
fi
OUTPUT="${NAME}-${OS}-${ARCH}${EXE}"
if [ "${ARCH}" = "x86_64" ]
then
ARCH="amd64"
fi
PACKAGE="$(grep module go.mod | awk '{print $2}')"
TAG="$(git describe --tags --abbrev=0)"
VERSION="$(echo ${TAG} | awk -F '-' '{print $NF}')"
COMMIT_HASH="$(git rev-parse --short HEAD)"
BUILD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
OUTPUT="${TAG}-${OS}-${ARCH}${EXE}"
LDFLAGS="${LDFLAGS} -X '${PACKAGE}/internal/version.Version=${VERSION}'"
LDFLAGS="${LDFLAGS} -X '${PACKAGE}/internal/version.CommitHash=${COMMIT_HASH}'"
LDFLAGS="${LDFLAGS} -X '${PACKAGE}/internal/version.BuildTimestamp=${BUILD_TIMESTAMP}'"
export CGO_ENABLED=1
export CC="${CC}"
@ -32,7 +44,6 @@ build(){
export GOARCH=$ARCH
go build -x -v \
-trimpath \
-tags static \
-ldflags="-s -w ${LDFLAGS_WIN}" \
-ldflags="-s -w ${LDFLAGS}" \
-o "${OUTPUT}"
}