├── .gitignore ├── LICENSE ├── README.md ├── arena ├── collision.go ├── disc.go └── main.go ├── build-modules.sh ├── build-nomodules.sh ├── demo ├── font │ └── main.go ├── invader │ ├── AndroidManifest.xml │ ├── assets │ │ ├── 95933__robinhood76__01665-thin-laser-blast.wav │ │ ├── 95933__robinhood76__01665-thin-laser-blast.wav.copyright.txt │ │ ├── icon-missile.png │ │ ├── icon-missile.png.copyright.txt │ │ ├── icon-right-left.png │ │ ├── icon-right-left.png.copyright.txt │ │ ├── icon.png │ │ ├── icon.png.copyright.txt │ │ ├── rocket.png │ │ ├── rocket.png.copyright.txt │ │ ├── server.txt │ │ ├── shader.frag │ │ ├── shader.vert │ │ ├── shader_tex.frag │ │ ├── shader_tex.vert │ │ ├── ship.png │ │ └── ship.png.copyright.txt │ ├── flag.go │ ├── font.go │ ├── main.go │ ├── paint.go │ ├── server.go │ └── texture.go ├── square │ └── main.go ├── triangle │ └── main.go └── triangle2 │ └── main.go ├── docs ├── adb_logcat.md ├── refs.md └── vec4.md ├── future ├── future.go └── future_test.go ├── go.mod ├── go.sum ├── msg └── msg.go ├── release.sh ├── trace └── trace.go ├── unit └── unit.go └── version └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 udhos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/udhos/fugo/blob/master/LICENSE) 2 | [![Go Report Card - invader](https://goreportcard.com/badge/github.com/udhos/fugo/invader)](https://goreportcard.com/report/github.com/udhos/fugo/invader) 3 | 4 | # fugo 5 | fugo - fun with Go. gomobile OpenGL game 6 | 7 | Table of Contents 8 | ================= 9 | 10 | * [QUICK START](#quick-start) 11 | * [Requirements](#requirements) 12 | * [Building the INVADER application](#building-the-invader-application) 13 | * [Building the ARENA server](#building-the-arena-server) 14 | * [How does the INVADER application locate the ARENA server?](#how-does-the-invader-application-locate-the-arena-server) 15 | * [INVADER runtime flags](#invader-runtime-flags) 16 | * [KNOWN ISSUES](#known-issues) 17 | 18 | Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go) 19 | 20 | ## QUICK START 21 | 22 | Recipe: 23 | 24 | go get github.com/udhos/fugo 25 | cd ~/go/src/github.com/udhos/fugo 26 | ./build.sh 27 | 28 | ## Requirements 29 | 30 | 1\. Install latest Go 31 | 32 | There are many other ways, this is a quick recipe: 33 | 34 | git clone github.com/udhos/update-golang 35 | cd update-golang 36 | sudo ./update-golang.sh 37 | 38 | 2\. Install Android NDK 39 | 40 | Download Android Studio - https://developer.android.com/studio 41 | 42 | Unzip Android Studio: 43 | 44 | $ tar xf /tmp/android-studio-ide-191.5791312-linux.tar.gz 45 | 46 | Run Android Studio: 47 | 48 | $ ~/android-studio/bin/studio.sh & 49 | 50 | Select: Configure -> SDK Manager -> SDK Tools -> NDK 51 | 52 | Click the Apply button. 53 | 54 | Define SDK env vars. For example: 55 | 56 | export ANDROID_HOME=~/Android/Sdk 57 | export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/20.0.5594570 ;# watch out the version 58 | 59 | 3\. Install gomobile 60 | 61 | Recipe: 62 | 63 | go get golang.org/x/mobile/cmd/gomobile 64 | gomobile version 65 | #gomobile init -ndk $NDK ;# no longer used? 66 | 67 | 4\. Install OpenGL dev libs 68 | 69 | On Ubuntu you will need these: 70 | 71 | sudo apt install libegl1-mesa-dev libgles2-mesa-dev libx11-dev 72 | 73 | 5\. Install alsa sound dev libs 74 | 75 | On Ubuntu you will need this: 76 | 77 | sudo apt install libasound2-dev 78 | 79 | 6\. Get fugo 80 | 81 | Recipe: 82 | 83 | go get github.com/udhos/fugo 84 | 85 | ## Building the INVADER application 86 | 87 | 7\. Build for desktop 88 | 89 | Recipe: 90 | 91 | go install -tags gldebug github.com/udhos/fugo/demo/invader 92 | 93 | Hint: You can test the desktop version by running 'invader': 94 | 95 | $ (cd demo/invader && invader slow) 96 | 97 | The parameter 'slow' sets a very low frame rate, useful for test/debugging. 98 | If you want smooth rendering, remove the parameter 'slow'. 99 | 100 | The subshell is used to temporarily enter the demo/invader dir in order to load assets from demo/invader/assets. 101 | 102 | 8\. Build for Android 103 | 104 | Recipe: 105 | 106 | gomobile build -target=android github.com/udhos/fugo/demo/invader 107 | 108 | Hint: Use 'gomobile build -x' to see what the build is doing. 109 | 110 | $ gomobile build -x github.com/udhos/fugo/demo/invader 111 | 112 | 9\. Push into Android device 113 | 114 | Recipe: 115 | 116 | gomobile install github.com/udhos/fugo/demo/invader 117 | 118 | ## Building the ARENA server 119 | 120 | 10\. Build the server 121 | 122 | Recipe: 123 | 124 | $ go install github.com/udhos/fugo/arena 125 | 126 | 10\. Run the server 127 | 128 | $ (cd demo/invader && arena) 129 | 130 | The arena server needs to load image information from demo/invader/assets. 131 | 132 | ## How does the INVADER application locate the ARENA server? 133 | 134 | The Invader application will continously try two methods to reach the server: 135 | 136 | a) The Invader application will send a discovery request to UDP 239.1.1.1:8888. If there is an Arena server in the LAN, it will respond reporting its TCP endpoint. This local discovery is useful for quickly deploying a local Arena server. It depends on multicasting on the local network. 137 | 138 | b) The Invader application will try to connect to the Arena server specified in the file server.txt: 139 | 140 | $ more demo/invader/assets/server.txt 141 | localhost:8080 142 | 143 | The TCP endpoint hard-coded in the file server.txt is included in the APK file. You will need to rebuild and redeploy the application to change it. This option is useful for deploying public Arena server on the Internet. 144 | 145 | ## INVADER runtime flags 146 | 147 | You can tweak the app behavior by changing these files before gomobile build: 148 | 149 | demo/invader/assets/box.txt - bool (file_exists=true) 150 | demo/invader/assets/server.txt - string host:port (TCP endpoint for server) 151 | demo/invader/assets/slow.txt - bool (file_exists=true) 152 | demo/invader/assets/trace.txt - string host:port (UDP endpoint for logs) 153 | 154 | ## KNOWN ISSUES 155 | 156 | ### x/mobile: build failing when using go modules 157 | 158 | https://github.com/golang/go/issues/27234 159 | 160 | ### Need way to hide Android status bar. Fixed: add the theme below to AndroidManifest.xml 161 | 162 | Add this to AndroidManifest.xml: 163 | 164 | 167 | 168 | https://github.com/golang/go/issues/12766 169 | 170 | https://github.com/golang/go/issues/21396 171 | 172 | ### Need way to set Android app icon. Fixed: add assets/icon.png 173 | 174 | https://github.com/golang/go/issues/9985 175 | 176 | https://golang.org/cl/30019 177 | 178 | ### Need way to call Android API from Go. 179 | 180 | Reverse Binding https://www.slideshare.net/takuyaueda967/mobile-apps-by-pure-go-with-reverse-binding 181 | 182 | slides 65-67 from https://pt.slideshare.net/takuyaueda967/go-for-mobile-games 183 | 184 | #### RunOnJVM 185 | 186 | RunOnJVM added to gomobile: 187 | 188 | - https://github.com/golang/go/issues/26815 189 | 190 | - https://golang.org/cl/127758 191 | 192 | Old info on RunOnJVM: https://gist.github.com/tenntenn/aae3d14d0df4884ac4e7 193 | 194 | ## References 195 | 196 | ### Just port a Golang game to Android 197 | 198 | https://dev.to/ntoooop/just-port-a-golang-game-to-android--3a9f 199 | 200 | ### Korok Game Engine 201 | 202 | https://korok.io/ 203 | 204 | https://github.com/KorokEngine/Korok 205 | 206 | --xx-- 207 | 208 | -------------------------------------------------------------------------------- /arena/collision.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"log" 5 | "time" 6 | 7 | "github.com/udhos/fugo/future" 8 | "github.com/udhos/fugo/unit" 9 | ) 10 | 11 | type box interface { 12 | Bounding() (float64, float64, float64, float64) 13 | } 14 | 15 | func intersect(b1, b2 box) bool { 16 | b1x1, b1y1, b1x2, b1y2 := b1.Bounding() 17 | b2x1, b2y1, b2x2, b2y2 := b2.Bounding() 18 | 19 | noOverlap := b1x1 > b2x2 || 20 | b2x1 > b1x2 || 21 | b1y1 > b2y2 || 22 | b2y1 > b1y2 23 | 24 | return !noOverlap 25 | } 26 | 27 | func detectCollision(w *world, now time.Time) bool { 28 | 29 | left := -1.0 30 | right := 1.0 31 | fieldTop := 1.0 32 | cannonBottom := -1.0 33 | hit := false 34 | 35 | NEXT_MISSILE: 36 | for i := 0; i < len(w.missileList); i++ { 37 | m := w.missileList[i] 38 | mY := float64(future.MissileY(m.CoordY, m.Speed, now.Sub(m.Start))) 39 | mUp := m.Team == 0 40 | mr := unit.MissileBox(left, right, float64(m.CoordX), mY, fieldTop, cannonBottom, w.cannonWidth, w.cannonHeight, w.missileWidth, w.missileHeight, mUp) 41 | 42 | for _, p := range w.playerTab { 43 | if p.cannonLife <= 0 { 44 | continue 45 | } 46 | if m.Team == p.team { 47 | continue 48 | } 49 | cX, _ := future.CannonX(p.cannonCoordX, p.cannonSpeed, now.Sub(p.cannonStart)) 50 | cUp := p.team == 0 51 | cr := unit.CannonBox(left, right, float64(cX), fieldTop, cannonBottom, w.cannonWidth, w.cannonHeight, cUp) 52 | if intersect(mr, cr) { 53 | //log.Printf("collision: %v %v", m, p) 54 | removeMissile(w, i) 55 | i-- 56 | hit = true 57 | p.cannonLife -= .25 58 | if p.cannonLife <= 0 { 59 | w.teams[m.Team].score++ 60 | p.cannonLife = 0 // cosmetic 61 | p.cannonCoordX = cX // cannon freeze 62 | p.cannonSpeed = 0 // cannon freeze 63 | } 64 | continue NEXT_MISSILE 65 | } 66 | } 67 | } 68 | 69 | return hit 70 | } 71 | -------------------------------------------------------------------------------- /arena/disc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | ) 7 | 8 | func lanDiscovery(addr string) error { 9 | 10 | listen := "239.1.1.1:8888" 11 | proto := "udp" 12 | 13 | log.Printf("discovery service reporting %s on %s %s", addr, proto, listen) 14 | 15 | udpAddr, errAddr := net.ResolveUDPAddr(proto, listen) 16 | if errAddr != nil { 17 | return errAddr 18 | } 19 | 20 | conn, errListen := net.ListenMulticastUDP(proto, nil, udpAddr) 21 | if errListen != nil { 22 | return errListen 23 | } 24 | 25 | go func() { 26 | buf := make([]byte, 1000) 27 | for { 28 | _, src, errRead := conn.ReadFromUDP(buf) 29 | if errRead != nil { 30 | log.Printf("discovery read error from %v: %v", src, errRead) 31 | continue 32 | } 33 | _, errWrite := conn.WriteTo([]byte(addr), src) 34 | if errWrite != nil { 35 | log.Printf("discovery write error to %v: %v", src, errWrite) 36 | continue 37 | } 38 | log.Printf("discovery: replied %s to %v", addr, src) 39 | } 40 | }() 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /arena/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/gob" 5 | "flag" 6 | "fmt" 7 | "image" 8 | _ "image/png" // The _ means to import a package purely for its initialization side effects. 9 | "log" 10 | "net" 11 | "os" 12 | "runtime" 13 | "sync/atomic" 14 | "time" 15 | 16 | "github.com/udhos/fugo/future" 17 | "github.com/udhos/fugo/msg" 18 | "github.com/udhos/fugo/unit" 19 | "github.com/udhos/fugo/version" 20 | ) 21 | 22 | type world struct { 23 | playerTab []*player 24 | playerAdd chan *player 25 | playerDel chan *player 26 | input chan inputMsg 27 | updateInterval time.Duration 28 | missileList []*msg.Missile 29 | teams [2]team 30 | cannonWidth float64 31 | cannonHeight float64 32 | missileWidth float64 33 | missileHeight float64 34 | countConn int32 35 | } 36 | 37 | type team struct { 38 | count int // player count 39 | score int // team score 40 | } 41 | 42 | type inputMsg struct { 43 | player *player 44 | msg interface{} 45 | } 46 | 47 | type player struct { 48 | conn net.Conn 49 | output chan msg.Update 50 | fuelStart time.Time 51 | cannonStart time.Time 52 | cannonSpeed float32 53 | cannonCoordX float32 54 | cannonLife float32 55 | cannonID int 56 | team int 57 | } 58 | 59 | func main() { 60 | 61 | log.Printf("arena version " + version.Version + " runtime " + runtime.Version()) 62 | 63 | var addr string 64 | 65 | flag.StringVar(&addr, "addr", ":8080", "listen address") 66 | 67 | flag.Parse() 68 | 69 | w := world{ 70 | playerTab: []*player{}, 71 | playerAdd: make(chan *player), 72 | playerDel: make(chan *player), 73 | updateInterval: 1000 * time.Millisecond, 74 | input: make(chan inputMsg), 75 | } 76 | 77 | cannon := "assets/ship.png" 78 | var errCanSz error 79 | w.cannonWidth, w.cannonHeight, errCanSz = loadSize(cannon, unit.ScaleCannon) 80 | if errCanSz != nil { 81 | log.Printf("collision will NOT work: %v", errCanSz) 82 | } 83 | log.Printf("cannon: %s: %vx%v", cannon, w.cannonWidth, w.cannonHeight) 84 | 85 | missile := "assets/rocket.png" 86 | var errMisSz error 87 | w.missileWidth, w.missileHeight, errMisSz = loadSize(missile, unit.ScaleMissile) 88 | if errMisSz != nil { 89 | log.Printf("collision will NOT work: %v", errMisSz) 90 | } 91 | log.Printf("missile: %s: %vx%v", missile, w.missileWidth, w.missileHeight) 92 | 93 | if errListen := listenAndServe(&w, addr); errListen != nil { 94 | log.Printf("main: listen: %v", errListen) 95 | return 96 | } 97 | 98 | if errDisc := lanDiscovery(addr); errDisc != nil { 99 | log.Printf("main: discovery: %v", errDisc) 100 | return 101 | } 102 | 103 | missileID := 0 104 | cannonID := 0 105 | 106 | tickerUpdate := time.NewTicker(w.updateInterval) 107 | tickerCollision := time.NewTicker(100 * time.Millisecond) 108 | 109 | log.Printf("main: entering service loop") 110 | SERVICE: 111 | for { 112 | select { 113 | case p := <-w.playerAdd: 114 | p.team = 0 115 | if w.teams[0].count > w.teams[1].count { 116 | p.team = 1 117 | } 118 | log.Printf("player add: %v team=%d team0=%d team1=%d", p, p.team, w.teams[0].count, w.teams[1].count) 119 | w.playerTab = append(w.playerTab, p) 120 | 121 | playerFuelSet(p, time.Now(), 5) // reset fuel to 50% 122 | p.cannonStart = p.fuelStart 123 | p.cannonSpeed = float32(.15) // 15% 124 | p.cannonCoordX = .5 // 50% 125 | p.cannonID = cannonID 126 | p.cannonLife = 1 // 100% 127 | cannonID++ 128 | w.teams[p.team].count++ 129 | case p := <-w.playerDel: 130 | log.Printf("player del: %v team=%d team0=%d team1=%d", p, p.team, w.teams[0].count, w.teams[1].count) 131 | for i, pl := range w.playerTab { 132 | if pl == p { 133 | //w.playerTab = append(w.playerTab[:i], w.playerTab[i+1:]...) 134 | if i < len(w.playerTab)-1 { 135 | w.playerTab[i] = w.playerTab[len(w.playerTab)-1] 136 | } 137 | w.playerTab = w.playerTab[:len(w.playerTab)-1] 138 | w.teams[p.team].count-- 139 | log.Printf("player removed: %v", p) 140 | continue SERVICE 141 | } 142 | } 143 | log.Printf("player not found: %v", p) 144 | case i := <-w.input: 145 | //log.Printf("input: %v", i) 146 | 147 | switch m := i.msg.(type) { 148 | case msg.Button: 149 | log.Printf("input button: %v", m) 150 | 151 | if i.player.cannonLife <= 0 { 152 | continue // cannon destroyed 153 | } 154 | 155 | if m.ID == msg.ButtonTurn { 156 | p := i.player 157 | updateCannon(p, time.Now()) 158 | p.cannonSpeed = -p.cannonSpeed 159 | updateWorld(&w, false) 160 | continue SERVICE 161 | } 162 | 163 | now := time.Now() 164 | fuel := playerFuel(i.player, now) 165 | 166 | if m.ID != msg.ButtonFire { 167 | continue SERVICE // non-fire button 168 | } 169 | 170 | if fuel < 1 { 171 | continue SERVICE // not enough fuel 172 | } 173 | 174 | playerFuelConsume(i.player, now, 1) 175 | 176 | updateCannon(i.player, now) 177 | miss1 := &msg.Missile{ 178 | ID: missileID, 179 | CoordX: i.player.cannonCoordX, 180 | Speed: .5, // 50% every 1 second 181 | Team: i.player.team, 182 | Start: now, 183 | } 184 | missileID++ 185 | w.missileList = append(w.missileList, miss1) 186 | 187 | log.Printf("input fire - fuel was=%v is=%v missiles=%d", fuel, playerFuel(i.player, now), len(w.missileList)) 188 | 189 | updateWorld(&w, true) 190 | } 191 | 192 | case <-tickerUpdate.C: 193 | //log.Printf("tick: %v", t) 194 | 195 | updateWorld(&w, false) 196 | case <-tickerCollision.C: 197 | if detectCollision(&w, time.Now()) { 198 | updateWorld(&w, false) 199 | } 200 | } 201 | } 202 | } 203 | 204 | func loadSize(name string, scale float64) (float64, float64, error) { 205 | bogus := image.Rect(0, 0, 10, 10) 206 | w, h := unit.BoxSize(bogus, scale) 207 | 208 | f, errOpen := os.Open(name) 209 | if errOpen != nil { 210 | return w, h, fmt.Errorf("loadSize: open: %s: %v", name, errOpen) 211 | } 212 | defer f.Close() 213 | img, _, errDec := image.Decode(f) 214 | if errDec != nil { 215 | return w, h, fmt.Errorf("loadSize: decode: %s: %v", name, errDec) 216 | } 217 | i, ok := img.(*image.NRGBA) 218 | if !ok { 219 | return w, h, fmt.Errorf("loadSize: %s: not NRGBA", name) 220 | } 221 | 222 | w, h = unit.BoxSize(i, scale) 223 | b := i.Bounds() 224 | 225 | log.Printf("loadSize: %s: %vx%v => %vx%v", name, b.Max.X, b.Max.Y, w, h) 226 | 227 | return w, h, nil 228 | } 229 | 230 | func updateCannon(p *player, now time.Time) { 231 | p.cannonCoordX, p.cannonSpeed = future.CannonX(p.cannonCoordX, p.cannonSpeed, time.Since(p.cannonStart)) 232 | p.cannonStart = now 233 | } 234 | 235 | func removeMissile(w *world, i int) { 236 | last := len(w.missileList) - 1 237 | if i < last { 238 | w.missileList[i] = w.missileList[last] 239 | } 240 | w.missileList = w.missileList[:last] 241 | } 242 | 243 | func updateWorld(w *world, fire bool) { 244 | now := time.Now() 245 | 246 | for _, p := range w.playerTab { 247 | updateCannon(p, now) 248 | } 249 | 250 | for i := 0; i < len(w.missileList); i++ { 251 | m := w.missileList[i] 252 | m.CoordY = future.MissileY(m.CoordY, m.Speed, time.Since(m.Start)) 253 | m.Start = now 254 | if m.CoordY >= 1 { 255 | removeMissile(w, i) 256 | i-- 257 | } 258 | } 259 | 260 | for _, p := range w.playerTab { 261 | sendUpdatesToPlayer(w, p, now, fire) 262 | } 263 | } 264 | 265 | func playerFuel(p *player, now time.Time) float32 { 266 | return future.Fuel(0, now.Sub(p.fuelStart)) 267 | } 268 | 269 | func playerFuelSet(p *player, now time.Time, fuel float32) { 270 | p.fuelStart = now.Add(-time.Duration(float32(time.Second) * fuel / future.FuelRechargeRate)) 271 | } 272 | 273 | func playerFuelConsume(p *player, now time.Time, amount float32) { 274 | fuel := playerFuel(p, now) 275 | playerFuelSet(p, now, fuel-amount) 276 | } 277 | 278 | func sendUpdatesToPlayer(w *world, p *player, now time.Time, fire bool) { 279 | update := msg.Update{ 280 | Fuel: playerFuel(p, now), 281 | Interval: w.updateInterval, 282 | WorldMissiles: w.missileList, 283 | Team: p.team, 284 | Scores: [2]int{w.teams[0].score, w.teams[1].score}, 285 | FireSound: fire, 286 | } 287 | 288 | for _, p1 := range w.playerTab { 289 | cannon := msg.Cannon{ 290 | ID: p1.cannonID, 291 | Start: p1.cannonStart, 292 | CoordX: p1.cannonCoordX, 293 | Speed: p1.cannonSpeed, 294 | Team: p1.team, 295 | Life: p1.cannonLife, 296 | Player: p1 == p, 297 | } 298 | update.Cannons = append(update.Cannons, &cannon) 299 | } 300 | 301 | //log.Printf("sending updates to player %v", p) 302 | 303 | p.output <- update 304 | } 305 | 306 | func listenAndServe(w *world, addr string) error { 307 | 308 | proto := "tcp" 309 | 310 | log.Printf("serving on %s %s", proto, addr) 311 | 312 | listener, errListen := net.Listen(proto, addr) 313 | if errListen != nil { 314 | return fmt.Errorf("listenAndServe: %s: %v", addr, errListen) 315 | } 316 | 317 | gob.Register(msg.Update{}) 318 | gob.Register(msg.Button{}) 319 | 320 | go func() { 321 | for { 322 | conn, err := listener.Accept() 323 | if err != nil { 324 | log.Printf("count=%d accept on TCP %s: %s", atomic.LoadInt32(&w.countConn), addr, err) 325 | conn.Close() 326 | continue 327 | } 328 | c, _ := conn.(*net.TCPConn) 329 | go connHandler(w, c) 330 | } 331 | }() 332 | 333 | return nil 334 | } 335 | 336 | func connHandler(w *world, conn *net.TCPConn) { 337 | count := atomic.AddInt32(&w.countConn, 1) 338 | log.Printf("count=%d connHandler %v", count, conn.RemoteAddr()) 339 | 340 | defer func() { 341 | c := atomic.AddInt32(&w.countConn, -1) 342 | log.Printf("count=%d connHandler exiting: %v", c, conn.RemoteAddr()) 343 | conn.Close() 344 | }() 345 | 346 | p := &player{ 347 | conn: conn, 348 | output: make(chan msg.Update), 349 | } 350 | 351 | w.playerAdd <- p // register player 352 | quitWriter := make(chan struct{}) 353 | 354 | go func() { 355 | // copy from socket into input channel 356 | dec := gob.NewDecoder(conn) 357 | for { 358 | var m msg.Button 359 | if err := dec.Decode(&m); err != nil { 360 | log.Printf("handler: Decode: %v", err) 361 | break 362 | } 363 | w.input <- inputMsg{player: p, msg: m} 364 | } 365 | close(quitWriter) // send quit request to output goroutine 366 | log.Printf("handler: reader goroutine exiting") 367 | }() 368 | 369 | // copy from output channel into socket 370 | enc := gob.NewEncoder(conn) 371 | LOOP: 372 | for { 373 | select { 374 | case <-quitWriter: 375 | log.Printf("handler: quit request") 376 | break LOOP 377 | case m := <-p.output: 378 | if err := enc.Encode(&m); err != nil { 379 | log.Printf("handler: Encode: %v", err) 380 | break LOOP 381 | } 382 | } 383 | } 384 | w.playerDel <- p // deregister player 385 | log.Printf("handler: writer goroutine exiting") 386 | } 387 | -------------------------------------------------------------------------------- /build-modules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | step=0 4 | 5 | msg() { 6 | step=$((step+1)) 7 | echo >&2 $step. $* 8 | } 9 | 10 | check() { 11 | local sub=$1 12 | 13 | msg fmt $sub 14 | gofmt -s -w $sub/*.go 15 | 16 | msg fix $sub 17 | go tool fix $sub/*.go 18 | 19 | msg vet $sub 20 | go tool vet $sub 21 | 22 | #msg gosimple $sub 23 | #hash gosimple && gosimple $sub/*.go 24 | 25 | msg golint $sub 26 | hash golint && golint $sub/*.go 27 | 28 | #msg staticcheck $sub 29 | #hash staticcheck && staticcheck $sub/*.go 30 | 31 | #msg unused $sub 32 | #hash unused && unused $sub/*.go 33 | 34 | msg test $sub 35 | go test $sub 36 | } 37 | 38 | build() { 39 | local sub=$1 40 | 41 | check $sub 42 | 43 | msg desktop install $sub 44 | go install $sub 45 | } 46 | 47 | mobilebuild() { 48 | local sub=$1 49 | 50 | build $sub 51 | 52 | msg android build $sub 53 | gomobile build -target=android $sub 54 | 55 | msg now use this command do push to android device: 56 | echo gomobile install $sub 57 | } 58 | 59 | check ./future 60 | check ./msg 61 | check ./trace 62 | check ./unit 63 | check ./version 64 | 65 | if [ "$1" != arena ]; then 66 | mobilebuild ./demo/square 67 | mobilebuild ./demo/triangle2 68 | mobilebuild ./demo/triangle 69 | mobilebuild ./demo/invader 70 | fi 71 | 72 | build ./demo/font 73 | build ./arena 74 | 75 | -------------------------------------------------------------------------------- /build-nomodules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | step=0 4 | 5 | msg() { 6 | step=$((step+1)) 7 | echo >&2 $step. $* 8 | } 9 | 10 | get() { 11 | i=$1 12 | msg fetching $i 13 | go get $i 14 | msg fetching $i - done 15 | } 16 | 17 | pkg=github.com/udhos/fugo 18 | 19 | check() { 20 | local sub=$1 21 | local full=$pkg/$sub 22 | 23 | msg fmt $sub 24 | gofmt -s -w $sub/*.go 25 | 26 | msg fix $sub 27 | go tool fix $sub/*.go 28 | 29 | msg vet $sub 30 | go tool vet $sub 31 | 32 | msg gosimple $sub 33 | hash gosimple && gosimple $sub/*.go 34 | 35 | msg golint $sub 36 | hash golint && golint $sub/*.go 37 | 38 | msg staticcheck $sub 39 | hash staticcheck && staticcheck $sub/*.go 40 | 41 | msg unused $sub 42 | hash unused && unused $sub/*.go 43 | 44 | msg test $full 45 | go test $full 46 | } 47 | 48 | build() { 49 | local sub=$1 50 | local full=$pkg/$sub 51 | 52 | check $sub 53 | 54 | msg desktop install $full 55 | go install $full 56 | } 57 | 58 | mobilebuild() { 59 | local sub=$1 60 | local full=$pkg/$sub 61 | 62 | build $sub 63 | 64 | msg android build $full 65 | gomobile build -target=android $full 66 | 67 | msg now use this command do push to android device: 68 | echo gomobile install $full 69 | } 70 | 71 | get honnef.co/go/tools/cmd/unused 72 | get honnef.co/go/tools/cmd/gosimple 73 | get honnef.co/go/tools/cmd/staticcheck 74 | get github.com/golang/lint/golint 75 | get github.com/udhos/pixfont 76 | get github.com/udhos/goglmath 77 | get golang.org/x/net/ipv4 78 | get github.com/faiface/beep 79 | get github.com/hajimehoshi/oto 80 | get github.com/pkg/errors 81 | 82 | check future 83 | check msg 84 | check trace 85 | check unit 86 | check version 87 | if [ "$1" != arena ]; then 88 | mobilebuild demo/square 89 | mobilebuild demo/triangle2 90 | mobilebuild demo/triangle 91 | mobilebuild demo/invader 92 | fi 93 | build demo/font 94 | build arena 95 | 96 | -------------------------------------------------------------------------------- /demo/font/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/png" 7 | "os" 8 | 9 | "github.com/udhos/pixfont" 10 | ) 11 | 12 | func main() { 13 | img := image.NewRGBA(image.Rect(0, 0, 150, 30)) 14 | 15 | pixfont.DrawString(img, 10, 10, "Hello, World!", color.Black) 16 | 17 | f, _ := os.OpenFile("hello.png", os.O_CREATE|os.O_RDWR, 0644) 18 | png.Encode(f, img) 19 | } 20 | -------------------------------------------------------------------------------- /demo/invader/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /demo/invader/assets/95933__robinhood76__01665-thin-laser-blast.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udhos/fugo/e44791f4c5fab7bc0314f99d69756780151c0866/demo/invader/assets/95933__robinhood76__01665-thin-laser-blast.wav -------------------------------------------------------------------------------- /demo/invader/assets/95933__robinhood76__01665-thin-laser-blast.wav.copyright.txt: -------------------------------------------------------------------------------- 1 | http://freesound.org/people/Robinhood76/sounds/95933 2 | -------------------------------------------------------------------------------- /demo/invader/assets/icon-missile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udhos/fugo/e44791f4c5fab7bc0314f99d69756780151c0866/demo/invader/assets/icon-missile.png -------------------------------------------------------------------------------- /demo/invader/assets/icon-missile.png.copyright.txt: -------------------------------------------------------------------------------- 1 | https://pngtree.com/freepng/missile_933819.html 2 | -------------------------------------------------------------------------------- /demo/invader/assets/icon-right-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udhos/fugo/e44791f4c5fab7bc0314f99d69756780151c0866/demo/invader/assets/icon-right-left.png -------------------------------------------------------------------------------- /demo/invader/assets/icon-right-left.png.copyright.txt: -------------------------------------------------------------------------------- 1 | http://findicons.com/icon/84762/transfer_left_right 2 | -------------------------------------------------------------------------------- /demo/invader/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udhos/fugo/e44791f4c5fab7bc0314f99d69756780151c0866/demo/invader/assets/icon.png -------------------------------------------------------------------------------- /demo/invader/assets/icon.png.copyright.txt: -------------------------------------------------------------------------------- 1 | http://www.freeiconspng.com/img/17260 2 | -------------------------------------------------------------------------------- /demo/invader/assets/rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udhos/fugo/e44791f4c5fab7bc0314f99d69756780151c0866/demo/invader/assets/rocket.png -------------------------------------------------------------------------------- /demo/invader/assets/rocket.png.copyright.txt: -------------------------------------------------------------------------------- 1 | https://pixabay.com/en/rocket-missile-lift-off-start-fire-146104 2 | -------------------------------------------------------------------------------- /demo/invader/assets/server.txt: -------------------------------------------------------------------------------- 1 | localhost:8080 2 | -------------------------------------------------------------------------------- /demo/invader/assets/shader.frag: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | uniform vec4 color; 4 | void main() { 5 | gl_FragColor = color; 6 | } 7 | -------------------------------------------------------------------------------- /demo/invader/assets/shader.vert: -------------------------------------------------------------------------------- 1 | #version 100 2 | attribute vec3 position; 3 | uniform mat4 P; 4 | void main() { 5 | gl_Position = P * vec4(position,1.0); 6 | } 7 | -------------------------------------------------------------------------------- /demo/invader/assets/shader_tex.frag: -------------------------------------------------------------------------------- 1 | #version 100 2 | precision mediump float; 3 | uniform sampler2D sampler; 4 | varying vec2 vTextureCoord; 5 | void main() { 6 | gl_FragColor = texture2D(sampler, vTextureCoord); 7 | } 8 | -------------------------------------------------------------------------------- /demo/invader/assets/shader_tex.vert: -------------------------------------------------------------------------------- 1 | #version 100 2 | attribute vec3 position; 3 | attribute vec2 textureCoord; 4 | uniform mat4 MVP; 5 | varying vec2 vTextureCoord; 6 | void main() { 7 | vTextureCoord = textureCoord; 8 | gl_Position = MVP * vec4(position,1.0); 9 | } 10 | -------------------------------------------------------------------------------- /demo/invader/assets/ship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udhos/fugo/e44791f4c5fab7bc0314f99d69756780151c0866/demo/invader/assets/ship.png -------------------------------------------------------------------------------- /demo/invader/assets/ship.png.copyright.txt: -------------------------------------------------------------------------------- 1 | http://www.freeiconspng.com/img/17260 2 | -------------------------------------------------------------------------------- /demo/invader/flag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | "golang.org/x/mobile/asset" 8 | ) 9 | 10 | func flagBool(value *bool, name string) { 11 | *value = exists(name) 12 | log.Printf("flagBool: %s = %v", name, *value) 13 | } 14 | 15 | func flagStr(value *string, name string) error { 16 | b, errLoad := loadFull(name) 17 | if errLoad != nil { 18 | log.Printf("flagStr: %s: %v", name, errLoad) 19 | return errLoad 20 | } 21 | *value = strings.TrimSpace(string(b)) 22 | log.Printf("flagStr: %s = [%v]", name, *value) 23 | return nil 24 | } 25 | 26 | func exists(name string) bool { 27 | f, errOpen := asset.Open(name) 28 | if errOpen != nil { 29 | return false 30 | } 31 | f.Close() 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /demo/invader/font.go: -------------------------------------------------------------------------------- 1 | // +build darwin linux windows 2 | 3 | package main 4 | 5 | import ( 6 | //"fmt" 7 | "bytes" 8 | "log" 9 | //"os" 10 | "encoding/binary" 11 | "image" 12 | "image/color" 13 | //"image/png" 14 | 15 | //"golang.org/x/mobile/asset" 16 | "golang.org/x/mobile/exp/f32" 17 | "golang.org/x/mobile/gl" 18 | 19 | "github.com/udhos/pixfont" 20 | ) 21 | 22 | type fontAtlas struct { 23 | glc gl.Context 24 | tex gl.Texture 25 | //vert gl.Buffer 26 | //elem gl.Buffer 27 | //elemCount int 28 | coordVer gl.Attrib 29 | coordTex gl.Attrib 30 | } 31 | 32 | type fontText struct { 33 | vert gl.Buffer 34 | elem gl.Buffer 35 | elemCount int 36 | atlas *fontAtlas 37 | } 38 | 39 | func newText(atlas *fontAtlas) *fontText { 40 | glc := atlas.glc 41 | t := &fontText{} 42 | t.atlas = atlas 43 | t.vert = glc.CreateBuffer() 44 | t.elem = glc.CreateBuffer() 45 | return t 46 | } 47 | 48 | func (t *fontText) delete() { 49 | t.atlas.glc.DeleteBuffer(t.vert) 50 | t.atlas.glc.DeleteBuffer(t.elem) 51 | } 52 | 53 | func (t *fontText) write(s string) { 54 | 55 | glc := t.atlas.glc // shortcut 56 | 57 | v := make([]float32, 0, 4*5*len(s)) 58 | e := make([]uint32, 0, len(s)) 59 | 60 | for i, b := range s { 61 | 62 | x1 := float32(i) 63 | x2 := x1 + 1.0 64 | 65 | c := b - fontFirst 66 | unit := 1.0 / float32(fontCount) 67 | s1 := float32(c) * unit 68 | s2 := float32(c+1) * unit 69 | 70 | v = append(v, 71 | // vert texture 72 | // --------- ------- 73 | x1, 1.0, 0.0, s1, 1.0, // 0 74 | x1, 0.0, 0.0, s1, 0.0, // 1 75 | x2, 0.0, 0.0, s2, 0.0, // 2 76 | x2, 1.0, 0.0, s2, 1.0, // 3 77 | ) 78 | 79 | j := 4 * uint32(i) 80 | 81 | e = append(e, 82 | j, j+1, j+2, // triangle 1 83 | j+2, j+3, j, // triangle 2 84 | ) 85 | 86 | } 87 | t.elemCount = len(e) 88 | 89 | bytesV := f32.Bytes(binary.LittleEndian, v...) 90 | bytesE := intsToBytes(e) 91 | 92 | glc.BindBuffer(gl.ARRAY_BUFFER, t.vert) 93 | glc.BufferData(gl.ARRAY_BUFFER, bytesV, gl.DYNAMIC_DRAW) 94 | 95 | glc.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, t.elem) 96 | glc.BufferData(gl.ELEMENT_ARRAY_BUFFER, bytesE, gl.DYNAMIC_DRAW) 97 | } 98 | 99 | func (t *fontText) draw() { 100 | 101 | glc := t.atlas.glc // shortcut 102 | 103 | glc.BindBuffer(gl.ARRAY_BUFFER, t.vert) 104 | glc.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, t.elem) 105 | 106 | elemFirst := 0 107 | elemCount := t.elemCount 108 | elemType := gl.Enum(gl.UNSIGNED_INT) 109 | elemSize := 4 // 4-byte int 110 | 111 | strideSize := 5 * 4 // 5 x 4 bytes (5 x 4-byte float) 112 | itemsPosition := 3 113 | itemsTexture := 2 114 | offsetPosition := 0 115 | offsetTexture := itemsPosition * 4 // 3 x 4 bytes 116 | 117 | glc.VertexAttribPointer(t.atlas.coordVer, itemsPosition, gl.FLOAT, false, strideSize, offsetPosition) 118 | glc.VertexAttribPointer(t.atlas.coordTex, itemsTexture, gl.FLOAT, false, strideSize, offsetTexture) 119 | 120 | glc.BindTexture(gl.TEXTURE_2D, t.atlas.tex) 121 | 122 | glc.DrawElements(gl.TRIANGLES, elemCount, elemType, elemFirst*elemSize) 123 | } 124 | 125 | const ( 126 | fontFirst = 32 127 | fontPastend = 127 128 | fontCount = fontPastend - fontFirst 129 | ) 130 | 131 | func newAtlas(glc gl.Context, c color.Color, coordVert, coordTex gl.Attrib) (*fontAtlas, error) { 132 | 133 | first := fontFirst 134 | pastend := fontPastend 135 | size := fontCount 136 | b := make([]byte, 0, size) 137 | buf := bytes.NewBuffer(b) 138 | 139 | for i := first; i < pastend; i++ { 140 | err := buf.WriteByte(byte(i)) 141 | if err != nil { 142 | log.Printf("newAtlas: %d %v", i, err) 143 | } 144 | } 145 | 146 | str := buf.String() 147 | 148 | log.Printf("newAtlas: chars=%d str=%d: [%v]", size, len(str), str) 149 | 150 | pixfont.Spacing = 0 // default: 1 pixel 151 | 152 | width := pixfont.MeasureString(str) 153 | log.Printf("newAtlas: width=%d", width) 154 | 155 | img := image.NewNRGBA(image.Rect(0, 0, width, 8)) 156 | 157 | bo := img.Bounds() 158 | fontWidth := width / size 159 | fontHeight := bo.Max.Y 160 | log.Printf("newAtlas: atlas=%dx%d => font=%dx%d", bo.Max.X, bo.Max.Y, fontWidth, fontHeight) 161 | 162 | //c := color.NRGBA{128,230,128,255} 163 | w := pixfont.DrawString(img, 0, 0, str, c) 164 | 165 | log.Printf("newAtlas: drawn %d pixels", w) 166 | 167 | /* 168 | f, errWr := os.OpenFile("atlas.png", os.O_CREATE|os.O_RDWR, 0644) 169 | if errWr != nil { 170 | log.Printf("IO: %v", errWr) 171 | } 172 | png.Encode(f, img) 173 | //f.Flush() 174 | f.Close() 175 | */ 176 | 177 | tex, errUpload := uploadImage(glc, "", img, true) 178 | if errUpload != nil { 179 | return nil, errUpload 180 | } 181 | 182 | a := &fontAtlas{} 183 | 184 | a.glc = glc 185 | a.tex = tex 186 | //a.vert = glc.CreateBuffer() 187 | //a.elem = glc.CreateBuffer() 188 | a.coordVer = coordVert 189 | a.coordTex = coordTex 190 | 191 | return a, nil 192 | } 193 | 194 | func (a *fontAtlas) delete() { 195 | a.glc.DeleteTexture(a.tex) 196 | //a.glc.DeleteBuffer(a.vert) 197 | //a.glc.DeleteBuffer(a.elem) 198 | } 199 | -------------------------------------------------------------------------------- /demo/invader/main.go: -------------------------------------------------------------------------------- 1 | // +build darwin linux windows 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "encoding/gob" 9 | "fmt" 10 | "image" 11 | "image/color" 12 | _ "image/png" // The _ means to import a package purely for its initialization side effects. 13 | "io/ioutil" 14 | "log" 15 | "os" 16 | "runtime" 17 | "strconv" 18 | "time" 19 | 20 | "github.com/udhos/goglmath" 21 | 22 | "golang.org/x/mobile/app" 23 | "golang.org/x/mobile/asset" 24 | "golang.org/x/mobile/event/lifecycle" 25 | "golang.org/x/mobile/event/mouse" 26 | "golang.org/x/mobile/event/paint" 27 | "golang.org/x/mobile/event/size" 28 | "golang.org/x/mobile/event/touch" 29 | "golang.org/x/mobile/exp/f32" 30 | "golang.org/x/mobile/exp/gl/glutil" 31 | "golang.org/x/mobile/gl" 32 | 33 | "github.com/faiface/beep" 34 | "github.com/faiface/beep/speaker" 35 | "github.com/faiface/beep/wav" 36 | 37 | "github.com/udhos/fugo/future" 38 | "github.com/udhos/fugo/msg" 39 | "github.com/udhos/fugo/trace" 40 | "github.com/udhos/fugo/unit" 41 | "github.com/udhos/fugo/version" 42 | ) 43 | 44 | type gameState struct { 45 | width int 46 | height int 47 | gl gl.Context 48 | program gl.Program 49 | programTex gl.Program 50 | bufSquare gl.Buffer 51 | bufSquareWire gl.Buffer 52 | bufCannon gl.Buffer 53 | bufCannonDown gl.Buffer 54 | bufSquareElemIndex gl.Buffer 55 | bufSquareElemData gl.Buffer 56 | 57 | // simple shader 58 | position gl.Attrib 59 | P gl.Uniform // projection mat4 uniform 60 | color gl.Uniform 61 | 62 | // texturizing shader 63 | texPosition gl.Attrib 64 | texTextureCoord gl.Attrib 65 | texSampler gl.Uniform 66 | texMVP gl.Uniform // MVP mat4 67 | texButtonFire gl.Texture 68 | texButtonTurn gl.Texture 69 | ship gl.Texture 70 | missile gl.Texture 71 | 72 | streamLaser beep.StreamSeekCloser 73 | 74 | cannonWidth float64 75 | cannonHeight float64 76 | missileWidth float64 77 | missileHeight float64 78 | debugBound bool 79 | 80 | atlas *fontAtlas 81 | t1 *fontText 82 | scoreOur *fontText 83 | scoreTheir *fontText 84 | 85 | minX, maxX, minY, maxY float64 86 | shaderVert string 87 | shaderFrag string 88 | shaderTexVert string 89 | shaderTexFrag string 90 | serverAddr string 91 | serverOutput chan msg.Button 92 | playerFuel float32 93 | playerTeam int 94 | updateInterval time.Duration 95 | updateLast time.Time 96 | missiles map[int]*msg.Missile 97 | cannons map[int]*msg.Cannon 98 | tracer *trace.Trace 99 | } 100 | 101 | func playLaser(game *gameState) { 102 | game.streamLaser.Seek(0) 103 | speaker.Play(beep.Seq(game.streamLaser)) 104 | } 105 | 106 | func loadSound(name string) (beep.StreamSeekCloser, error) { 107 | f, errSndLaser := asset.Open(name) 108 | if errSndLaser != nil { 109 | return nil, errSndLaser 110 | } 111 | 112 | s, format, errDec := wav.Decode(f) 113 | if errDec != nil { 114 | return nil, errDec 115 | } 116 | 117 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 118 | 119 | return s, nil 120 | } 121 | 122 | func newGame() (*gameState, error) { 123 | game := &gameState{ 124 | minX: -1, 125 | maxX: 1, 126 | minY: -1, 127 | maxY: 1, 128 | missiles: map[int]*msg.Missile{}, 129 | cannons: map[int]*msg.Cannon{}, 130 | } 131 | 132 | sndLaser := "95933__robinhood76__01665-thin-laser-blast.wav" 133 | var errSndLaser error 134 | game.streamLaser, errSndLaser = loadSound(sndLaser) 135 | if errSndLaser != nil { 136 | log.Printf("laser sound: %v", errSndLaser) 137 | } 138 | 139 | if errVert := flagStr(&game.shaderVert, "shader.vert"); errVert != nil { 140 | log.Printf("load vertex shader: %v", errVert) 141 | return nil, errVert 142 | } 143 | 144 | if errFrag := flagStr(&game.shaderFrag, "shader.frag"); errFrag != nil { 145 | log.Printf("load fragment shader: %v", errFrag) 146 | return nil, errFrag 147 | } 148 | 149 | if errVert := flagStr(&game.shaderTexVert, "shader_tex.vert"); errVert != nil { 150 | log.Printf("load vertex tex shader: %v", errVert) 151 | return nil, errVert 152 | } 153 | 154 | if errFrag := flagStr(&game.shaderTexFrag, "shader_tex.frag"); errFrag != nil { 155 | log.Printf("load fragment tex shader: %v", errFrag) 156 | return nil, errFrag 157 | } 158 | 159 | if errServ := flagStr(&game.serverAddr, "server.txt"); errServ != nil { 160 | log.Printf("load server: %v", errServ) 161 | return nil, errServ 162 | } 163 | 164 | log.Printf("server: [%s]", game.serverAddr) 165 | 166 | var tracer string 167 | errTrace := flagStr(&tracer, "trace.txt") 168 | if errTrace != nil { 169 | log.Printf("trace file: %v", errTrace) 170 | } else { 171 | log.Printf("tracer: [%s]", tracer) 172 | game.tracer, errTrace = trace.New(tracer) 173 | if errTrace != nil { 174 | log.Printf("trace sock: %v", errTrace) 175 | } 176 | } 177 | log.Printf("tracer: %v", game.tracer) 178 | 179 | flagBool(&game.debugBound, "box.txt") 180 | 181 | game.tracef("trace, hello from invader app") 182 | 183 | game.updateInterval = 2 * time.Second 184 | game.updateLast = time.Now() 185 | 186 | game.serverOutput = make(chan msg.Button) 187 | 188 | return game, nil 189 | } 190 | 191 | func flipY(name string, img *image.NRGBA) { 192 | b := img.Bounds() 193 | midY := (b.Max.Y - b.Min.Y) / 2 194 | for x := b.Min.X; x < b.Max.X; x++ { 195 | for y1 := b.Min.Y; y1 < midY; y1++ { 196 | y2 := b.Max.Y - y1 - 1 197 | c1 := img.At(x, y1) 198 | c2 := img.At(x, y2) 199 | img.Set(x, y1, c2) 200 | img.Set(x, y2, c1) 201 | } 202 | } 203 | log.Printf("image y-flipped: %s", name) 204 | } 205 | 206 | func loadID() { 207 | wd, errWd := os.Getwd() 208 | log.Printf("loadID: dir=%s error=%v", wd, errWd) 209 | idFile := "invader_id.txt" 210 | f, errOpen := os.Open(idFile) 211 | if errOpen != nil { 212 | log.Printf("loadID: %s: %v", idFile, errOpen) 213 | return 214 | } 215 | defer f.Close() 216 | buf, errRead := ioutil.ReadAll(f) 217 | if errRead != nil { 218 | log.Printf("loadID: %s: %v", idFile, errRead) 219 | return 220 | } 221 | id := string(buf) 222 | log.Printf("loadID: id=%s", id) 223 | } 224 | 225 | func saveID() { 226 | wd, errWd := os.Getwd() 227 | log.Printf("saveID: dir=%s error=%v", wd, errWd) 228 | idFile := "invader_id.txt" 229 | f, errOpen := os.Create(idFile) 230 | if errOpen != nil { 231 | log.Printf("saveID: %s: %v", idFile, errOpen) 232 | return 233 | } 234 | defer f.Close() 235 | id := "3" 236 | _, errWrite := f.WriteString(id) 237 | if errWrite != nil { 238 | log.Printf("saveID: %s: %v", idFile, errWrite) 239 | return 240 | } 241 | log.Printf("saveID: id=%s", id) 242 | } 243 | 244 | func main() { 245 | log.Print("main begin") 246 | log.Print("fugo invader version " + version.Version + " runtime " + runtime.Version()) 247 | 248 | slowPaint := len(os.Args) > 1 249 | if !slowPaint { 250 | flagBool(&slowPaint, "slow.txt") 251 | } 252 | log.Printf("slowPaint: %v", slowPaint) 253 | 254 | var paintRequests int 255 | var paints int 256 | sec := time.Now().Second() 257 | game, errGame := newGame() 258 | if errGame != nil { 259 | log.Printf("main: fatal: %v", errGame) 260 | return 261 | } 262 | 263 | gob.Register(msg.Update{}) 264 | gob.Register(msg.Button{}) 265 | 266 | loadID() 267 | saveID() 268 | 269 | app.Main(func(a app.App) { 270 | log.Print("app.Main begin") 271 | 272 | go serverHandler(a, game.serverAddr, game.serverOutput) 273 | 274 | LOOP: 275 | for e := range a.Events() { 276 | switch t := a.Filter(e).(type) { 277 | case lifecycle.Event: 278 | log.Printf("Lifecycle: %v", t) 279 | 280 | if t.From > t.To && t.To == lifecycle.StageDead { 281 | log.Printf("lifecycle down to dead") 282 | break LOOP 283 | } 284 | 285 | if t.Crosses(lifecycle.StageAlive) == lifecycle.CrossOff { 286 | log.Printf("lifecycle cross down alive") 287 | break LOOP 288 | } 289 | 290 | switch t.Crosses(lifecycle.StageVisible) { 291 | case lifecycle.CrossOn: 292 | glc, isGL := t.DrawContext.(gl.Context) 293 | if !isGL { 294 | log.Printf("Lifecycle: visible: bad GL context") 295 | continue LOOP 296 | } 297 | game.start(glc) 298 | a.Send(paint.Event{}) // start drawing 299 | case lifecycle.CrossOff: 300 | game.stop() 301 | } 302 | 303 | case paint.Event: 304 | if t.External || game.gl == nil { 305 | // As we are actively painting as fast as 306 | // we can (usually 60 FPS), skip any paint 307 | // events sent by the system. 308 | continue 309 | } 310 | 311 | paintRequests++ 312 | 313 | if now := time.Now().Second(); now != sec { 314 | // once per second event 315 | log.Printf("requests: %d, paints: %d, team=%d", paintRequests, paints, game.playerTeam) 316 | paintRequests = 0 317 | paints = 0 318 | sec = now 319 | } 320 | 321 | //if !slowPaint || paintRequests == 0 { 322 | paints++ 323 | game.paint() 324 | a.Publish() 325 | //} 326 | 327 | if slowPaint { 328 | time.Sleep(200 * time.Millisecond) // slow down paint event request 329 | } 330 | 331 | // we request next paint event 332 | // in order to draw as fast as possible 333 | a.Send(paint.Event{}) 334 | case mouse.Event: 335 | press := (t.Direction & 1) == 1 336 | release := (t.Direction & 2) == 2 337 | game.input(press, release, t.X, t.Y) 338 | case touch.Event: 339 | press := t.Type == touch.TypeBegin 340 | release := t.Type == touch.TypeEnd 341 | game.input(press, release, t.X, t.Y) 342 | case size.Event: 343 | game.resize(t.WidthPx, t.HeightPx) 344 | case msg.Update: 345 | //log.Printf("app.Main event update: %v", t) 346 | game.playerTeam = t.Team 347 | game.playerFuel = t.Fuel 348 | game.updateInterval = t.Interval 349 | 350 | game.updateLast = time.Now() 351 | elap := time.Since(game.updateLast) 352 | 353 | missiles := map[int]*msg.Missile{} 354 | for _, m := range t.WorldMissiles { 355 | old, found := game.missiles[m.ID] 356 | if found { 357 | oldY := future.MissileY(old.CoordY, old.Speed, elap) 358 | newY := future.MissileY(m.CoordY, m.Speed, elap) 359 | if newY < oldY { 360 | // refuse to move back in time 361 | missiles[m.ID] = old // prevent deletion 362 | continue 363 | } 364 | } 365 | missiles[m.ID] = m 366 | } 367 | game.missiles = missiles 368 | 369 | cannons := map[int]*msg.Cannon{} 370 | for _, c := range t.Cannons { 371 | old, found := game.cannons[c.ID] 372 | if found { 373 | if old.Speed == c.Speed { 374 | oldX, _ := future.CannonX(old.CoordX, old.Speed, elap) 375 | newX, _ := future.CannonX(c.CoordX, c.Speed, elap) 376 | if (old.Speed >= 0 && newX < oldX) || (old.Speed < 0 && newX > oldX) { 377 | // refuse to move back in time 378 | cannons[c.ID] = old // prevent deletion 379 | continue 380 | } 381 | } 382 | } 383 | cannons[c.ID] = c 384 | } 385 | game.cannons = cannons 386 | 387 | game.t1.write(fmt.Sprintf("%f", t.Fuel)) 388 | 389 | var our, their string 390 | our = strconv.Itoa(t.Scores[t.Team]) 391 | their = strconv.Itoa(t.Scores[1-t.Team]) 392 | game.scoreOur.write(our) 393 | game.scoreTheir.write(their) 394 | 395 | if t.FireSound { 396 | playLaser(game) 397 | } 398 | } 399 | } 400 | 401 | log.Print("app.Main end") 402 | }) 403 | 404 | log.Print("main end") 405 | } 406 | 407 | func loadFull(name string) ([]byte, error) { 408 | f, errOpen := asset.Open(name) 409 | if errOpen != nil { 410 | return nil, errOpen 411 | } 412 | defer f.Close() 413 | buf, errRead := ioutil.ReadAll(f) 414 | if errRead != nil { 415 | return nil, errRead 416 | } 417 | log.Printf("loaded: %s (%d bytes)", name, len(buf)) 418 | return buf, nil 419 | } 420 | 421 | func (game *gameState) tracef(format string, v ...interface{}) { 422 | if game.tracer == nil { 423 | return 424 | } 425 | game.tracer.Printf(format, v...) 426 | } 427 | 428 | func (game *gameState) resize(w, h int) { 429 | if game.width != w || game.height != h { 430 | log.Printf("resize: %d,%d", w, h) 431 | } 432 | game.width = w 433 | game.height = h 434 | 435 | if h >= w { 436 | aspect := float64(h) / float64(w) 437 | game.minX = -1 438 | game.maxX = 1 439 | game.minY = -aspect 440 | game.maxY = aspect 441 | } else { 442 | aspect := float64(w) / float64(h) 443 | game.minX = -aspect 444 | game.maxX = aspect 445 | game.minY = -1 446 | game.maxY = 1 447 | } 448 | 449 | log.Printf("resize: %v,%v,%v,%v", game.minX, game.maxX, game.minY, game.maxY) 450 | 451 | glc := game.gl // shortcut 452 | if glc == nil { 453 | return 454 | } 455 | 456 | glc.Viewport(0, 0, w, h) 457 | } 458 | 459 | func (game *gameState) input(press, release bool, pixelX, pixelY float32) { 460 | log.Printf("input: event press=%v %f,%f (%d x %d)", press, pixelX, pixelY, game.width, game.height) 461 | 462 | if press { 463 | y := float64(pixelY)/float64(game.height-1)*(game.minY-game.maxY) + game.maxY 464 | 465 | if y < (game.minY + game.buttonEdge()) { 466 | // might hit button 467 | pixelsPerButton := float32(game.width) / float32(buttons) 468 | b := pixelX / pixelsPerButton 469 | game.serverOutput <- msg.Button{ID: int(b)} 470 | } 471 | } 472 | } 473 | 474 | func (game *gameState) start(glc gl.Context) { 475 | log.Printf("start") 476 | 477 | var err error 478 | game.program, err = glutil.CreateProgram(glc, game.shaderVert, game.shaderFrag) 479 | if err != nil { 480 | log.Printf("start: error creating GL program: %v", err) 481 | return 482 | } 483 | log.Printf("start: shader compiled") 484 | 485 | var errTex error 486 | game.programTex, errTex = glutil.CreateProgram(glc, game.shaderTexVert, game.shaderTexFrag) 487 | if errTex != nil { 488 | log.Printf("start: error creating GL texturizer program: %v", errTex) 489 | return 490 | } 491 | log.Printf("start: texturizing shader compiled") 492 | 493 | game.bufSquare = glc.CreateBuffer() 494 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufSquare) 495 | glc.BufferData(gl.ARRAY_BUFFER, squareData, gl.STATIC_DRAW) 496 | 497 | game.bufSquareWire = glc.CreateBuffer() 498 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufSquareWire) 499 | glc.BufferData(gl.ARRAY_BUFFER, squareWireData, gl.STATIC_DRAW) 500 | 501 | game.bufCannon = glc.CreateBuffer() 502 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufCannon) 503 | glc.BufferData(gl.ARRAY_BUFFER, cannonData, gl.STATIC_DRAW) 504 | 505 | game.bufCannonDown = glc.CreateBuffer() 506 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufCannonDown) 507 | glc.BufferData(gl.ARRAY_BUFFER, cannonDownData, gl.STATIC_DRAW) 508 | 509 | game.position = getAttribLocation(glc, game.program, "position") 510 | game.P = getUniformLocation(glc, game.program, "P") 511 | game.color = getUniformLocation(glc, game.program, "color") 512 | 513 | game.texPosition = getAttribLocation(glc, game.programTex, "position") 514 | game.texTextureCoord = getAttribLocation(glc, game.programTex, "textureCoord") 515 | game.texMVP = getUniformLocation(glc, game.programTex, "MVP") 516 | game.texSampler = getUniformLocation(glc, game.programTex, "sampler") 517 | 518 | var errLoad error 519 | game.texButtonFire, _, errLoad = loadTexture(glc, "icon-missile.png", true) 520 | if errLoad != nil { 521 | log.Printf("start: texture load: %v", errLoad) 522 | } 523 | game.texButtonTurn, _, errLoad = loadTexture(glc, "icon-right-left.png", true) 524 | if errLoad != nil { 525 | log.Printf("start: texture load: %v", errLoad) 526 | } 527 | var shipImg *image.NRGBA 528 | game.ship, shipImg, errLoad = loadTexture(glc, "ship.png", true) 529 | if errLoad != nil { 530 | log.Printf("start: texture load: %v", errLoad) 531 | } 532 | 533 | game.cannonWidth, game.cannonHeight = unit.BoxSize(shipImg, unit.ScaleCannon) 534 | 535 | var missImg *image.NRGBA 536 | game.missile, missImg, errLoad = loadTexture(glc, "rocket.png", true) 537 | if errLoad != nil { 538 | log.Printf("start: texture load: %v", errLoad) 539 | } 540 | 541 | game.missileWidth, game.missileHeight = unit.BoxSize(missImg, unit.ScaleMissile) 542 | 543 | game.bufSquareElemData = glc.CreateBuffer() 544 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufSquareElemData) 545 | glc.BufferData(gl.ARRAY_BUFFER, squareElemData, gl.STATIC_DRAW) 546 | 547 | game.bufSquareElemIndex = glc.CreateBuffer() 548 | glc.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, game.bufSquareElemIndex) 549 | glc.BufferData(gl.ELEMENT_ARRAY_BUFFER, squareElemIndex, gl.STATIC_DRAW) 550 | 551 | var errFont error 552 | game.atlas, errFont = newAtlas(glc, color.NRGBA{128, 230, 128, 255}, game.texPosition, game.texTextureCoord) 553 | if errFont != nil { 554 | log.Printf("start: font: %v", errFont) 555 | } 556 | 557 | game.t1 = newText(game.atlas) 558 | game.t1.write("invader") 559 | game.scoreOur = newText(game.atlas) 560 | game.scoreOur.write("?") 561 | game.scoreTheir = newText(game.atlas) 562 | game.scoreTheir.write("?") 563 | 564 | glc.ClearColor(.5, .5, .5, 1) // gray background 565 | glc.ClearDepthf(1) // default 566 | glc.Enable(gl.DEPTH_TEST) // enable depth testing 567 | glc.DepthFunc(gl.LEQUAL) // gl.LESS is default depth test 568 | glc.DepthRangef(0, 1) // default 569 | 570 | game.gl = glc 571 | 572 | log.Printf("start: shaders initialized") 573 | } 574 | 575 | func getUniformLocation(glc gl.Context, prog gl.Program, uniform string) gl.Uniform { 576 | location := glc.GetUniformLocation(prog, uniform) 577 | if location.Value < 0 { 578 | log.Printf("bad uniform '%s' location: %d", uniform, location.Value) 579 | } 580 | return location 581 | } 582 | 583 | func getAttribLocation(glc gl.Context, prog gl.Program, attr string) gl.Attrib { 584 | location := glc.GetAttribLocation(prog, attr) 585 | // FIXME 1000 is a hack to detect a bad location.Value, since it can't represent -1 586 | if location.Value > 1000 { 587 | log.Printf("bad attribute '%s' location: %d", attr, location.Value) 588 | } 589 | return location 590 | } 591 | 592 | func (game *gameState) stop() { 593 | log.Printf("stop") 594 | 595 | glc := game.gl // shortcut 596 | 597 | if game.scoreOur != nil { 598 | game.scoreOur.delete() 599 | game.scoreOur = nil 600 | } 601 | 602 | if game.scoreTheir != nil { 603 | game.scoreTheir.delete() 604 | game.scoreTheir = nil 605 | } 606 | 607 | if game.t1 != nil { 608 | game.t1.delete() 609 | game.t1 = nil 610 | } 611 | 612 | if game.atlas != nil { 613 | game.atlas.delete() 614 | game.atlas = nil 615 | } 616 | 617 | glc.DeleteProgram(game.program) 618 | glc.DeleteProgram(game.programTex) 619 | glc.DeleteTexture(game.texButtonFire) 620 | glc.DeleteTexture(game.texButtonTurn) 621 | glc.DeleteTexture(game.ship) 622 | glc.DeleteTexture(game.missile) 623 | glc.DeleteBuffer(game.bufSquareElemIndex) 624 | glc.DeleteBuffer(game.bufSquareElemData) 625 | 626 | glc.DeleteBuffer(game.bufSquare) 627 | glc.DeleteBuffer(game.bufSquareWire) 628 | glc.DeleteBuffer(game.bufCannon) 629 | glc.DeleteBuffer(game.bufCannonDown) 630 | 631 | game.gl = nil 632 | 633 | log.Printf("stop: shader disposed") 634 | } 635 | 636 | func (game *gameState) setOrtho(m *goglmath.Matrix4) { 637 | // near=1 far=-1 -> keep Z 638 | // near=-1 far=1 -> flip Z 639 | goglmath.SetOrthoMatrix(m, game.minX, game.maxX, game.minY, game.maxY, -1, 1) 640 | } 641 | 642 | const buttons = 5 643 | 644 | func (game *gameState) buttonEdge() float64 { 645 | screenWidth := game.maxX - game.minX 646 | return screenWidth / float64(buttons) 647 | } 648 | 649 | const ( 650 | coordsPerVertex = 3 651 | squareVertexCount = 6 652 | squareWireVertexCount = 4 653 | ) 654 | 655 | var cannonData = f32.Bytes(binary.LittleEndian, 656 | 0.5, 1.0, 0.0, 657 | 0.0, 0.0, 0.0, 658 | 1.0, 0.0, 0.0, 659 | ) 660 | 661 | var cannonDownData = f32.Bytes(binary.LittleEndian, 662 | 0.5, 0.0, 0.0, 663 | 1.0, 1.0, 0.0, 664 | 0.0, 1.0, 0.0, 665 | ) 666 | 667 | var squareData = f32.Bytes(binary.LittleEndian, 668 | 0.0, 1.0, 0.0, 669 | 0.0, 0.0, 0.0, 670 | 1.0, 0.0, 0.0, 671 | 1.0, 0.0, 0.0, 672 | 1.0, 1.0, 0.0, 673 | 0.0, 1.0, 0.0, 674 | ) 675 | 676 | var squareWireData = f32.Bytes(binary.LittleEndian, 677 | 0.0, 1.0, 0.0, 678 | 0.0, 0.0, 0.0, 679 | 1.0, 0.0, 0.0, 680 | 1.0, 1.0, 0.0, 681 | ) 682 | 683 | const squareElemIndexCount = 6 684 | 685 | var squareElemIndex = intsToBytes([]uint32{ 686 | 0, 1, 2, 687 | 2, 3, 0, 688 | }) 689 | 690 | func intsToBytes(s []uint32) []byte { 691 | buf := new(bytes.Buffer) 692 | binary.Write(buf, binary.LittleEndian, s) 693 | b := buf.Bytes() 694 | return b 695 | } 696 | 697 | var squareElemData = f32.Bytes(binary.LittleEndian, 698 | // pos tex 699 | // ---------- -------- 700 | 0.0, 1.0, 0.0, 0.0, 1.0, // 0 701 | 0.0, 0.0, 0.0, 0.0, 0.0, // 1 702 | 1.0, 0.0, 0.0, 1.0, 0.0, // 2 703 | 1.0, 1.0, 0.0, 1.0, 1.0, // 3 704 | ) 705 | -------------------------------------------------------------------------------- /demo/invader/paint.go: -------------------------------------------------------------------------------- 1 | // +build darwin linux windows 2 | 3 | package main 4 | 5 | import ( 6 | //"log" 7 | "time" 8 | 9 | "github.com/udhos/goglmath" 10 | 11 | "golang.org/x/mobile/gl" 12 | 13 | "github.com/udhos/fugo/future" 14 | "github.com/udhos/fugo/unit" 15 | ) 16 | 17 | func (game *gameState) paint() { 18 | glc := game.gl // shortcut 19 | 20 | elap := time.Since(game.updateLast) 21 | 22 | glc.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 23 | 24 | glc.UseProgram(game.program) 25 | glc.EnableVertexAttribArray(game.position) 26 | 27 | glc.Uniform4f(game.color, .5, .9, .5, 1) // green 28 | 29 | screenWidth := game.maxX - game.minX 30 | screenHeight := game.maxY - game.minY 31 | fuelHeight := .05 32 | statusBarHeight := .14 33 | scoreTop := game.maxY - statusBarHeight 34 | scoreBarHeight := .06 35 | fieldTop := scoreTop - scoreBarHeight 36 | 37 | buttonWidth := game.buttonEdge() 38 | buttonHeight := buttonWidth 39 | 40 | // clamp height 41 | maxH := .3 * screenHeight 42 | if buttonHeight > maxH { 43 | buttonHeight = maxH 44 | } 45 | 46 | for i := 0; i < buttons; i++ { 47 | //squareWireMVP := goglmath.NewMatrix4Identity() 48 | var squareWireMVP goglmath.Matrix4 49 | game.setOrtho(&squareWireMVP) 50 | x := game.minX + float64(i)*buttonWidth 51 | squareWireMVP.Translate(x, game.minY, .1, 1) // z=.1 put in front of fuel bar 52 | squareWireMVP.Scale(buttonWidth, buttonHeight, 1, 1) 53 | glc.UniformMatrix4fv(game.P, squareWireMVP.Data()) 54 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufSquareWire) 55 | glc.VertexAttribPointer(game.position, coordsPerVertex, gl.FLOAT, false, 0, 0) 56 | glc.DrawArrays(gl.LINE_LOOP, 0, squareWireVertexCount) 57 | } 58 | 59 | fuelBottom := game.minY + buttonHeight 60 | 61 | // Wire rectangle around fuel bar 62 | fuelBarR := unit.Rect{X1: game.minX, Y1: fuelBottom, X2: game.minX + screenWidth, Y2: fuelBottom + fuelHeight} 63 | game.drawWireRect(fuelBarR, .5, .9, .5, 1, .1) 64 | 65 | // Fuel bar 66 | fuel := float64(future.Fuel(game.playerFuel, elap)) 67 | fuelR := unit.Rect{X1: game.minX, Y1: fuelBottom, X2: game.minX + screenWidth*fuel/10, Y2: fuelBottom + fuelHeight} 68 | game.drawRect(fuelR, .9, .9, .9, 1, 0) 69 | 70 | cannonBottom := fuelBottom + fuelHeight + .01 71 | 72 | // Cannons 73 | for _, can := range game.cannons { 74 | switch { 75 | case can.Life <= 0: 76 | glc.Uniform4f(game.color, .9, .2, .2, 1) // red - dead 77 | case can.Player: 78 | glc.Uniform4f(game.color, .2, .2, .8, 1) // blue - player 79 | default: 80 | glc.Uniform4f(game.color, .5, .9, .5, 1) // green - other 81 | } 82 | 83 | cannonX, _ := future.CannonX(can.CoordX, can.Speed, elap) 84 | 85 | up := can.Team == game.playerTeam 86 | 87 | r := unit.CannonBox(game.minX, game.maxX, float64(cannonX), fieldTop, cannonBottom, game.cannonWidth, game.cannonHeight, up) 88 | 89 | // life bar 90 | lifeBarH := .02 91 | lifeR := r 92 | lifeR.X2 = lifeR.X1 + game.cannonWidth*float64(can.Life) 93 | lifeR2 := r 94 | lifeR2.X1 = lifeR.X2 95 | if up { 96 | lifeR.Y2 = lifeR.Y1 + lifeBarH 97 | lifeR2.Y2 = lifeR.Y2 98 | } else { 99 | lifeR.Y1 = lifeR.Y2 - lifeBarH 100 | lifeR2.Y1 = lifeR.Y1 101 | } 102 | game.drawRect(lifeR, .4, .7, .9, 1, .05) 103 | game.drawRect(lifeR2, .9, .5, .5, 1, .05) 104 | 105 | if game.debugBound { 106 | game.drawWireRect(r, 1, 1, 1, 1, .1) 107 | } 108 | } 109 | 110 | // Missiles 111 | for _, miss := range game.missiles { 112 | up := miss.Team == game.playerTeam 113 | y := float64(future.MissileY(miss.CoordY, miss.Speed, elap)) 114 | 115 | r := unit.MissileBox(game.minX, game.maxX, float64(miss.CoordX), y, fieldTop, cannonBottom, game.cannonWidth, game.cannonHeight, game.missileWidth, game.missileHeight, up) 116 | 117 | if game.debugBound { 118 | game.drawWireRect(r, 1, 1, 1, 1, .1) 119 | } 120 | } 121 | 122 | glc.DisableVertexAttribArray(game.position) 123 | 124 | game.paintTex(glc, elap, buttonWidth, buttonHeight, scoreTop, scoreBarHeight, fieldTop, cannonBottom) // another shader 125 | } 126 | 127 | func (game *gameState) drawRect(rect unit.Rect, r, g, b, a float32, z float64) { 128 | glc := game.gl // shortcut 129 | 130 | glc.Uniform4f(game.color, r, g, b, a) 131 | 132 | var squareMVP goglmath.Matrix4 133 | game.setOrtho(&squareMVP) 134 | squareMVP.Translate(rect.X1, rect.Y1, z, 1) 135 | squareMVP.Scale(rect.X2-rect.X1, rect.Y2-rect.Y1, 1, 1) 136 | glc.UniformMatrix4fv(game.P, squareMVP.Data()) 137 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufSquare) 138 | glc.VertexAttribPointer(game.position, coordsPerVertex, gl.FLOAT, false, 0, 0) 139 | glc.DrawArrays(gl.TRIANGLES, 0, squareVertexCount) 140 | } 141 | 142 | func (game *gameState) drawWireRect(rect unit.Rect, r, g, b, a float32, z float64) { 143 | glc := game.gl // shortcut 144 | 145 | glc.Uniform4f(game.color, r, g, b, a) 146 | 147 | var squareWireMVP goglmath.Matrix4 148 | game.setOrtho(&squareWireMVP) 149 | squareWireMVP.Translate(rect.X1, rect.Y1, z, 1) 150 | squareWireMVP.Scale(rect.X2-rect.X1, rect.Y2-rect.Y1, 1, 1) 151 | glc.UniformMatrix4fv(game.P, squareWireMVP.Data()) 152 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufSquareWire) 153 | glc.VertexAttribPointer(game.position, coordsPerVertex, gl.FLOAT, false, 0, 0) 154 | glc.DrawArrays(gl.LINE_LOOP, 0, squareWireVertexCount) 155 | } 156 | 157 | func (game *gameState) paintTex(glc gl.Context, elap time.Duration, buttonWidth, buttonHeight, scoreTop, scoreHeight, fieldTop, cannonBottom float64) { 158 | 159 | glc.Enable(gl.BLEND) 160 | glc.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) 161 | 162 | glc.UseProgram(game.programTex) 163 | glc.EnableVertexAttribArray(game.texPosition) 164 | glc.EnableVertexAttribArray(game.texTextureCoord) 165 | 166 | tunit := 0 167 | glc.ActiveTexture(gl.TEXTURE0 + gl.Enum(tunit)) 168 | glc.Uniform1i(game.texSampler, tunit) 169 | 170 | // draw button - fire 171 | 172 | fireIndex := 0 173 | scaleButtonFire := buttonHeight // FIXME using square -- should use image aspect? 174 | xFire := game.minX + float64(fireIndex)*buttonWidth 175 | game.drawImage(game.texButtonFire, xFire, game.minY, scaleButtonFire, scaleButtonFire, 0, 1, 0) 176 | 177 | // draw button - turn 178 | 179 | turnIndex := 1 180 | scaleButtonTurn := buttonHeight // FIXME using square -- should use image aspect? 181 | xTurn := game.minX + float64(turnIndex)*buttonWidth 182 | game.drawImage(game.texButtonTurn, xTurn, game.minY, scaleButtonTurn, scaleButtonTurn, 0, 1, 0) 183 | 184 | // Cannons 185 | for _, can := range game.cannons { 186 | 187 | cannonX, _ := future.CannonX(can.CoordX, can.Speed, elap) 188 | 189 | up := can.Team == game.playerTeam 190 | 191 | r := unit.CannonBox(game.minX, game.maxX, float64(cannonX), fieldTop, cannonBottom, game.cannonWidth, game.cannonHeight, up) 192 | 193 | var upX, upY, upZ float64 194 | if up { 195 | upX = 0 196 | upY = 1 197 | upZ = 0 198 | } else { 199 | upX = 0 200 | upY = -1 201 | upZ = 0 202 | } 203 | game.drawImage(game.ship, r.X1, r.Y1, game.cannonWidth, game.cannonHeight, upX, upY, upZ) 204 | } 205 | 206 | // Missiles 207 | for _, miss := range game.missiles { 208 | up := miss.Team == game.playerTeam 209 | y := float64(future.MissileY(miss.CoordY, miss.Speed, elap)) 210 | 211 | r := unit.MissileBox(game.minX, game.maxX, float64(miss.CoordX), y, fieldTop, cannonBottom, game.cannonWidth, game.cannonHeight, game.missileWidth, game.missileHeight, up) 212 | 213 | var upY float64 214 | if up { 215 | upY = 1 216 | } else { 217 | upY = -1 218 | } 219 | game.drawImage(game.missile, r.X1, r.Y1, game.missileWidth, game.missileHeight, 0, upY, 0) 220 | } 221 | 222 | // font 223 | 224 | var MVPfont goglmath.Matrix4 225 | game.setOrtho(&MVPfont) 226 | MVPfont.Translate(0, 0, 0, 1) 227 | MVPfont.Scale(.1, .1, 1, 1) 228 | glc.UniformMatrix4fv(game.texMVP, MVPfont.Data()) 229 | 230 | game.t1.draw() 231 | 232 | // score 233 | var MVP goglmath.Matrix4 234 | scaleFont := scoreHeight 235 | scoreY := scoreTop - scaleFont 236 | 237 | game.setOrtho(&MVP) 238 | MVP.Translate(game.minX, scoreY, 0, 1) 239 | MVP.Scale(scaleFont, scaleFont, 1, 1) 240 | glc.UniformMatrix4fv(game.texMVP, MVP.Data()) 241 | game.scoreOur.draw() 242 | 243 | game.setOrtho(&MVP) 244 | MVP.Translate(0, scoreY, 0, 1) // FIXME coord X 245 | MVP.Scale(scaleFont, scaleFont, 1, 1) 246 | glc.UniformMatrix4fv(game.texMVP, MVP.Data()) 247 | game.scoreTheir.draw() 248 | 249 | // clean-up 250 | 251 | glc.DisableVertexAttribArray(game.texPosition) 252 | glc.DisableVertexAttribArray(game.texTextureCoord) 253 | 254 | glc.Disable(gl.BLEND) 255 | } 256 | 257 | func (game *gameState) drawImage(tex gl.Texture, x, y, width, height, upX, upY, upZ float64) { 258 | glc := game.gl // shortcut 259 | 260 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufSquareElemData) 261 | glc.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, game.bufSquareElemIndex) 262 | 263 | // square geometry 264 | elemFirst := 0 265 | elemCount := squareElemIndexCount // 6 266 | elemType := gl.Enum(gl.UNSIGNED_INT) 267 | elemSize := 4 268 | 269 | strideSize := 5 * 4 // 5 x 4 bytes 270 | itemsPosition := 3 271 | itemsTexture := 2 272 | offsetPosition := 0 273 | offsetTexture := itemsPosition * 4 // 3 x 4 bytes 274 | 275 | glc.VertexAttribPointer(game.texPosition, itemsPosition, gl.FLOAT, false, strideSize, offsetPosition) 276 | glc.VertexAttribPointer(game.texTextureCoord, itemsTexture, gl.FLOAT, false, strideSize, offsetTexture) 277 | 278 | var MVP goglmath.Matrix4 279 | game.setOrtho(&MVP) // 6. MVP = O 280 | MVP.Translate(x, y, 0, 1) // 5. MVP = O*T 281 | MVP.Scale(width, height, 1, 1) // 4. MVP = O*T*S 282 | MVP.Translate(.5, .5, 0, 1) // 3. MVP = O*T*S*t2 t2: restore center position 283 | MVP.Rotate(0, 0, -1, upX, upY, upZ) // 2. MVP = O*T*S*t2*R 284 | MVP.Translate(-.5, -.5, 0, 1) // 1. MVP = O*T*S*t2*R*t1 t1: translate center to origin 285 | 286 | glc.UniformMatrix4fv(game.texMVP, MVP.Data()) 287 | 288 | glc.BindTexture(gl.TEXTURE_2D, tex) 289 | glc.DrawElements(gl.TRIANGLES, elemCount, elemType, elemFirst*elemSize) 290 | } 291 | -------------------------------------------------------------------------------- /demo/invader/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/gob" 5 | "log" 6 | "net" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "golang.org/x/mobile/app" 12 | "golang.org/x/net/ipv4" 13 | 14 | "github.com/udhos/fugo/msg" 15 | ) 16 | 17 | func serverHandler(a app.App, serverAddr string, output <-chan msg.Button) { 18 | log.Printf("serverHandler: starting %s", serverAddr) 19 | 20 | // the reconnect loop switches between trying to connect to: 21 | // 1. address returned as response for UDP request to 239.1.1.1:8888 -- enables easy arena server on LAN 22 | // 2. serverAddr (loaded from file assets/server.txt) -- enables connection to public server 23 | discovery := false 24 | 25 | // reconnect loop 26 | for { 27 | server := serverAddr 28 | discovery = !discovery 29 | if discovery { 30 | addr, errReq := request() // request to LAN discovery at 239.1.1.1:8888 31 | if errReq == nil { 32 | server = addr 33 | log.Printf("serverHandler: discovery: %s", server) 34 | } else { 35 | log.Printf("serverHandler: discovery error: %v", errReq) 36 | } 37 | } 38 | 39 | proto := "tcp" 40 | 41 | log.Printf("serverHandler: opening %s %s", proto, server) 42 | conn, errDial := net.DialTimeout(proto, server, 2*time.Second) 43 | if errDial != nil { 44 | log.Printf("serverHandler: error %s: %v", server, errDial) 45 | } else { 46 | log.Printf("serverHandler: connected %s", server) 47 | quitWriter := make(chan struct{}) 48 | go writeLoop(conn, quitWriter, output) // spawn writer 49 | readLoop(a, conn) // loop reader 50 | conn.Close() 51 | close(quitWriter) 52 | } 53 | 54 | time.Sleep(2 * time.Second) // reconnect delay - do not hammer the server 55 | } 56 | } 57 | 58 | func request() (string, error) { 59 | timeout := 2 * time.Second 60 | 61 | discAddr := "239.1.1.1:8888" 62 | 63 | destAddr, errDest := net.ResolveUDPAddr("udp", discAddr) 64 | if errDest != nil { 65 | return "", errDest 66 | } 67 | 68 | conn, errListen := net.ListenUDP("udp", nil) 69 | if errListen != nil { 70 | return "", errListen 71 | } 72 | 73 | pc := ipv4.NewPacketConn(conn) 74 | 75 | if errLoop := pc.SetMulticastLoopback(true); errLoop != nil { 76 | log.Printf("SetMulticastLoopback error: %v", errLoop) 77 | } 78 | 79 | if errTTL := pc.SetTTL(5); errTTL != nil { 80 | log.Printf("SetTTL error: %v", errTTL) 81 | } 82 | 83 | if errSet := conn.SetDeadline(time.Now().Add(timeout)); errSet != nil { 84 | return "", errSet 85 | } 86 | 87 | _, errWrite := conn.WriteTo([]byte("request\n"), destAddr) 88 | if errWrite != nil { 89 | return "", errWrite 90 | } 91 | 92 | log.Printf("discovery request sent to %s", discAddr) 93 | 94 | buf := make([]byte, 1000) 95 | 96 | if errSet := conn.SetDeadline(time.Now().Add(timeout)); errSet != nil { 97 | return "", errSet 98 | } 99 | 100 | n, src, errRead := conn.ReadFrom(buf) 101 | if errRead != nil { 102 | return "", errRead 103 | } 104 | 105 | listen := strings.TrimSpace(string(buf[:n])) 106 | 107 | log.Printf("discovery response received: src=%s listen=%s", src.String(), listen) 108 | 109 | srcAddr, errSrc := net.ResolveUDPAddr("udp", src.String()) 110 | if errSrc != nil { 111 | return "", errSrc 112 | } 113 | srcHost := srcAddr.IP.String() 114 | 115 | listenAddr, errAddr := net.ResolveUDPAddr("udp", listen) 116 | if errAddr != nil { 117 | return "", errAddr 118 | } 119 | listenHost := listenAddr.IP.String() 120 | if listenHost != "" { 121 | if listenAddr.IP.To4() == nil { 122 | // IPv6 123 | srcHost = "[" + listenHost + "]" 124 | } else { 125 | // IPv4 126 | srcHost = listenHost 127 | } 128 | } 129 | 130 | endpoint := srcHost + ":" + strconv.Itoa(listenAddr.Port) 131 | 132 | return endpoint, nil 133 | } 134 | 135 | func readLoop(a app.App, conn net.Conn) { 136 | log.Printf("readLoop: entering") 137 | // copy from socket into event channel 138 | dec := gob.NewDecoder(conn) 139 | for { 140 | var m msg.Update 141 | if err := dec.Decode(&m); err != nil { 142 | log.Printf("readLoop: Decode: %v", err) 143 | break 144 | } 145 | a.Send(m) 146 | } 147 | log.Printf("readLoop: exiting") 148 | } 149 | 150 | func writeLoop(conn net.Conn, quit <-chan struct{}, output <-chan msg.Button) { 151 | log.Printf("writeLoop: goroutine starting") 152 | // copy from output channel into socket 153 | enc := gob.NewEncoder(conn) 154 | LOOP: 155 | for { 156 | select { 157 | case <-quit: 158 | log.Printf("writeLoop: quit request") 159 | break LOOP 160 | case m := <-output: 161 | if err := enc.Encode(&m); err != nil { 162 | log.Printf("writeLoop: Encode: %v", err) 163 | break LOOP 164 | } 165 | } 166 | } 167 | log.Printf("writeLoop: goroutine exiting") 168 | } 169 | -------------------------------------------------------------------------------- /demo/invader/texture.go: -------------------------------------------------------------------------------- 1 | // +build darwin linux windows 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | 9 | "image" 10 | _ "image/png" // The _ means to import a package purely for its initialization side effects. 11 | 12 | "golang.org/x/mobile/asset" 13 | "golang.org/x/mobile/gl" 14 | ) 15 | 16 | var nilTexture = gl.Texture{Value: 0xFFFFFFFF} 17 | 18 | func loadTexture(glc gl.Context, name string, yflip bool) (gl.Texture, *image.NRGBA, error) { 19 | 20 | imgFile := name 21 | imgIn, errImg := asset.Open(imgFile) 22 | if errImg != nil { 23 | return nilTexture, nil, fmt.Errorf("open texture image: %s: %v", imgFile, errImg) 24 | } 25 | img, _, errDec := image.Decode(imgIn) 26 | if errDec != nil { 27 | return nilTexture, nil, fmt.Errorf("decode texture image: %s: %v", imgFile, errDec) 28 | } 29 | if img == nil { 30 | return nilTexture, nil, fmt.Errorf("decode texture image: %s: nil", imgFile) 31 | } 32 | log.Printf("texture image loaded: %s", imgFile) 33 | i, ok := img.(*image.NRGBA) 34 | if !ok { 35 | return nilTexture, nil, fmt.Errorf("unexpected image type: %s: %v", imgFile, img.ColorModel()) 36 | } 37 | 38 | t, errUpload := uploadImage(glc, name, i, yflip) 39 | 40 | return t, i, errUpload 41 | } 42 | 43 | func uploadImage(glc gl.Context, name string, i *image.NRGBA, yflip bool) (gl.Texture, error) { 44 | b := i.Bounds() 45 | log.Printf("NRGBA image: %s %dx%d", name, b.Max.X, b.Max.Y) 46 | if yflip { 47 | flipY(name, i) 48 | } 49 | 50 | t := glc.CreateTexture() 51 | glc.BindTexture(gl.TEXTURE_2D, t) 52 | glc.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) 53 | glc.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) 54 | glc.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 55 | glc.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 56 | bounds := i.Bounds() 57 | w := bounds.Max.X - bounds.Min.X 58 | h := bounds.Max.Y - bounds.Min.Y 59 | 60 | // https://godoc.org/golang.org/x/mobile/gl 61 | // TexImage2D(target Enum, level int, internalFormat int, width, height int, format Enum, ty Enum, data []byte) 62 | glc.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, gl.RGBA, gl.UNSIGNED_BYTE, i.Pix) 63 | 64 | log.Printf("texture image uploaded: %s %dx%d", name, w, h) 65 | 66 | return t, nil 67 | } 68 | -------------------------------------------------------------------------------- /demo/square/main.go: -------------------------------------------------------------------------------- 1 | // +build darwin linux windows 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "golang.org/x/mobile/app" 13 | "golang.org/x/mobile/event/lifecycle" 14 | "golang.org/x/mobile/event/mouse" 15 | "golang.org/x/mobile/event/paint" 16 | "golang.org/x/mobile/event/size" 17 | "golang.org/x/mobile/event/touch" 18 | "golang.org/x/mobile/exp/f32" 19 | "golang.org/x/mobile/exp/gl/glutil" 20 | "golang.org/x/mobile/gl" 21 | ) 22 | 23 | type gameState struct { 24 | width int 25 | height int 26 | gl gl.Context 27 | program gl.Program 28 | bufData gl.Buffer 29 | bufIndex gl.Buffer 30 | position gl.Attrib 31 | } 32 | 33 | func main() { 34 | log.Print("main begin") 35 | 36 | slowPaint := len(os.Args) > 1 37 | log.Printf("slowPaint: %v", slowPaint) 38 | 39 | game := &gameState{} 40 | var requests int 41 | var paints int 42 | sec := time.Now().Second() 43 | 44 | app.Main(func(a app.App) { 45 | log.Print("app.Main begin") 46 | 47 | LOOP: 48 | for e := range a.Events() { 49 | switch t := a.Filter(e).(type) { 50 | case lifecycle.Event: 51 | log.Printf("Lifecycle: %v", t) 52 | 53 | if t.From > t.To && t.To == lifecycle.StageDead { 54 | log.Printf("lifecycle down to dead") 55 | break LOOP 56 | } 57 | 58 | if t.Crosses(lifecycle.StageAlive) == lifecycle.CrossOff { 59 | log.Printf("lifecycle cross down alive") 60 | break LOOP 61 | } 62 | 63 | switch t.Crosses(lifecycle.StageVisible) { 64 | case lifecycle.CrossOn: 65 | glc, isGL := t.DrawContext.(gl.Context) 66 | if !isGL { 67 | log.Printf("Lifecycle: visible: bad GL context") 68 | continue LOOP 69 | } 70 | game.start(glc) 71 | a.Send(paint.Event{}) // start drawing 72 | case lifecycle.CrossOff: 73 | game.stop() 74 | } 75 | 76 | case paint.Event: 77 | if t.External || game.gl == nil { 78 | // As we are actively painting as fast as 79 | // we can (usually 60 FPS), skip any paint 80 | // events sent by the system. 81 | continue 82 | } 83 | 84 | requests++ // events 85 | 86 | if now := time.Now().Second(); now != sec { 87 | log.Printf("requests=%d paints=%d", requests, paints) 88 | requests = 0 89 | paints = 0 90 | sec = now 91 | } 92 | 93 | //if !slowPaint || frames == 0 { 94 | paints++ // draws 95 | game.paint() 96 | a.Publish() 97 | //} 98 | 99 | if slowPaint { 100 | time.Sleep(250 * time.Millisecond) 101 | } 102 | 103 | // we request next paint event 104 | // in order to draw as fast as possible 105 | a.Send(paint.Event{}) 106 | case mouse.Event: 107 | game.input(t.X, t.Y) 108 | case touch.Event: 109 | game.input(t.X, t.Y) 110 | case size.Event: 111 | game.resize(t.WidthPx, t.HeightPx) 112 | } 113 | } 114 | 115 | log.Print("app.Main end") 116 | }) 117 | 118 | log.Print("main end") 119 | } 120 | 121 | func (game *gameState) resize(w, h int) { 122 | if game.width != w || game.height != h { 123 | log.Printf("resize: %d,%d", w, h) 124 | } 125 | game.width = w 126 | game.height = h 127 | } 128 | 129 | func (game *gameState) input(x, y float32) { 130 | log.Printf("input: %f,%f (%d x %d)", x, y, game.width, game.height) 131 | } 132 | 133 | func (game *gameState) start(glc gl.Context) { 134 | log.Printf("start") 135 | 136 | var err error 137 | game.program, err = glutil.CreateProgram(glc, vertexShader, fragmentShader) 138 | if err != nil { 139 | log.Printf("start: error creating GL program: %v", err) 140 | return 141 | } 142 | 143 | log.Printf("start: shader compiled") 144 | 145 | game.bufData = glc.CreateBuffer() 146 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufData) 147 | glc.BufferData(gl.ARRAY_BUFFER, squareData, gl.STATIC_DRAW) 148 | 149 | game.bufIndex = glc.CreateBuffer() 150 | glc.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, game.bufIndex) 151 | glc.BufferData(gl.ELEMENT_ARRAY_BUFFER, squareIndex, gl.STATIC_DRAW) 152 | 153 | game.position = glc.GetAttribLocation(game.program, "position") 154 | 155 | game.gl = glc 156 | } 157 | 158 | func (game *gameState) stop() { 159 | log.Printf("stop") 160 | 161 | glc := game.gl // shortcut 162 | 163 | glc.DeleteProgram(game.program) 164 | glc.DeleteBuffer(game.bufData) 165 | glc.DeleteBuffer(game.bufIndex) 166 | 167 | game.gl = nil 168 | } 169 | 170 | func (game *gameState) paint() { 171 | glc := game.gl // shortcut 172 | 173 | glc.ClearColor(.5, .5, .5, 1) // gray background 174 | glc.Clear(gl.COLOR_BUFFER_BIT) 175 | 176 | glc.UseProgram(game.program) 177 | glc.EnableVertexAttribArray(game.position) 178 | 179 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufData) 180 | glc.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, game.bufIndex) 181 | 182 | strideSize := 5 * 4 183 | offset := 0 184 | glc.VertexAttribPointer(game.position, coordsPerVertex, gl.FLOAT, false, strideSize, offset) 185 | 186 | //glc.DrawArrays(gl.TRIANGLES, 0, vertexCount) 187 | elemFirst := 0 188 | elemCount := vertexCount 189 | elemType := gl.Enum(gl.UNSIGNED_INT) // 32-bit int = 4 bytes 190 | elemSize := 4 // 32-bit int = 4 bytes 191 | glc.DrawElements(gl.TRIANGLES, elemCount, elemType, elemFirst*elemSize) 192 | 193 | glc.DisableVertexAttribArray(game.position) 194 | } 195 | 196 | const ( 197 | coordsPerVertex = 3 198 | vertexCount = 6 199 | ) 200 | 201 | var squareData = f32.Bytes(binary.LittleEndian, 202 | // position texture 203 | // ---------- -------- 204 | 0.0, 0.4, 0.0, 0.0, 0.4, // top left 205 | 0.0, 0.0, 0.0, 0.0, 0.0, // bottom left 206 | 0.4, 0.0, 0.0, 0.4, 0.0, // bottom right 207 | 0.4, 0.4, 0.0, 0.4, 0.4, // top right 208 | ) 209 | 210 | var squareIndex = intsToBytes([]uint32{ 211 | 0, 1, 2, 212 | 2, 3, 0, 213 | }) 214 | 215 | func intsToBytes(s []uint32) []byte { 216 | buf := new(bytes.Buffer) 217 | binary.Write(buf, binary.LittleEndian, s) 218 | b := buf.Bytes() 219 | log.Printf("intsToBytes: ints=%d bytes=%d: %v", len(s), len(b), b) 220 | return b 221 | } 222 | 223 | const vertexShader = `#version 100 224 | attribute vec4 position; 225 | void main() { 226 | gl_Position = position; 227 | }` 228 | 229 | const fragmentShader = `#version 100 230 | precision mediump float; 231 | void main() { 232 | gl_FragColor = vec4(0.8,0.8,0.8,1.0); // white 233 | }` 234 | -------------------------------------------------------------------------------- /demo/triangle/main.go: -------------------------------------------------------------------------------- 1 | // +build darwin linux windows 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/binary" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "golang.org/x/mobile/app" 12 | "golang.org/x/mobile/event/lifecycle" 13 | "golang.org/x/mobile/event/mouse" 14 | "golang.org/x/mobile/event/paint" 15 | "golang.org/x/mobile/event/size" 16 | "golang.org/x/mobile/event/touch" 17 | "golang.org/x/mobile/exp/f32" 18 | "golang.org/x/mobile/exp/gl/glutil" 19 | "golang.org/x/mobile/gl" 20 | ) 21 | 22 | type gameState struct { 23 | width int 24 | height int 25 | gl gl.Context 26 | program gl.Program 27 | buf gl.Buffer 28 | position gl.Attrib 29 | } 30 | 31 | func main() { 32 | log.Print("main begin") 33 | 34 | slowPaint := len(os.Args) > 1 35 | log.Printf("slowPaint: %v", slowPaint) 36 | 37 | game := &gameState{} 38 | var frames int 39 | var paints int 40 | sec := time.Now().Second() 41 | 42 | app.Main(func(a app.App) { 43 | log.Print("app.Main begin") 44 | 45 | LOOP: 46 | for e := range a.Events() { 47 | switch t := a.Filter(e).(type) { 48 | case lifecycle.Event: 49 | log.Printf("Lifecycle: %v", t) 50 | 51 | if t.From > t.To && t.To == lifecycle.StageDead { 52 | log.Printf("lifecycle down to dead") 53 | break LOOP 54 | } 55 | 56 | if t.Crosses(lifecycle.StageAlive) == lifecycle.CrossOff { 57 | log.Printf("lifecycle cross down alive") 58 | break LOOP 59 | } 60 | 61 | switch t.Crosses(lifecycle.StageVisible) { 62 | case lifecycle.CrossOn: 63 | glc, isGL := t.DrawContext.(gl.Context) 64 | if !isGL { 65 | log.Printf("Lifecycle: visible: bad GL context") 66 | continue LOOP 67 | } 68 | game.start(glc) 69 | a.Send(paint.Event{}) // start drawing 70 | case lifecycle.CrossOff: 71 | game.stop() 72 | } 73 | 74 | case paint.Event: 75 | if t.External || game.gl == nil { 76 | // As we are actively painting as fast as 77 | // we can (usually 60 FPS), skip any paint 78 | // events sent by the system. 79 | continue 80 | } 81 | 82 | paints++ // events 83 | 84 | if now := time.Now().Second(); now != sec { 85 | log.Printf("fps: %d, paints: %d", frames, paints) 86 | frames = 0 87 | paints = 0 88 | sec = now 89 | } 90 | 91 | if !slowPaint || frames == 0 { 92 | frames++ // draws 93 | game.paint() 94 | a.Publish() 95 | } 96 | 97 | if slowPaint { 98 | time.Sleep(500 * time.Millisecond) 99 | } 100 | 101 | // we request next paint event 102 | // in order to draw as fast as possible 103 | a.Send(paint.Event{}) 104 | case mouse.Event: 105 | game.input(t.X, t.Y) 106 | case touch.Event: 107 | game.input(t.X, t.Y) 108 | case size.Event: 109 | game.resize(t.WidthPx, t.HeightPx) 110 | } 111 | } 112 | 113 | log.Print("app.Main end") 114 | }) 115 | 116 | log.Print("main end") 117 | } 118 | 119 | func (game *gameState) resize(w, h int) { 120 | if game.width != w || game.height != h { 121 | log.Printf("resize: %d,%d", w, h) 122 | } 123 | game.width = w 124 | game.height = h 125 | } 126 | 127 | func (game *gameState) input(x, y float32) { 128 | log.Printf("input: %f,%f (%d x %d)", x, y, game.width, game.height) 129 | } 130 | 131 | func (game *gameState) start(glc gl.Context) { 132 | log.Printf("start") 133 | 134 | var err error 135 | game.program, err = glutil.CreateProgram(glc, vertexShader, fragmentShader) 136 | if err != nil { 137 | log.Printf("start: error creating GL program: %v", err) 138 | return 139 | } 140 | 141 | log.Printf("start: shader compiled") 142 | 143 | game.buf = glc.CreateBuffer() 144 | glc.BindBuffer(gl.ARRAY_BUFFER, game.buf) 145 | glc.BufferData(gl.ARRAY_BUFFER, triangleData, gl.STATIC_DRAW) 146 | 147 | game.position = glc.GetAttribLocation(game.program, "position") 148 | 149 | game.gl = glc 150 | } 151 | 152 | func (game *gameState) stop() { 153 | log.Printf("stop") 154 | 155 | glc := game.gl // shortcut 156 | 157 | glc.DeleteProgram(game.program) 158 | glc.DeleteBuffer(game.buf) 159 | 160 | game.gl = nil 161 | } 162 | 163 | func (game *gameState) paint() { 164 | //log.Printf("paint: call OpenGL here") 165 | glc := game.gl // shortcut 166 | 167 | glc.ClearColor(.5, .5, .5, 1) // gray background 168 | glc.Clear(gl.COLOR_BUFFER_BIT) 169 | 170 | glc.UseProgram(game.program) 171 | glc.EnableVertexAttribArray(game.position) 172 | 173 | glc.BindBuffer(gl.ARRAY_BUFFER, game.buf) 174 | 175 | // how to get data for location attribute within buffer 176 | glc.VertexAttribPointer(game.position, coordsPerVertex, gl.FLOAT, false, 0, 0) 177 | 178 | glc.DrawArrays(gl.TRIANGLES, 0, vertexCount) 179 | 180 | glc.DisableVertexAttribArray(game.position) 181 | } 182 | 183 | const ( 184 | coordsPerVertex = 3 185 | vertexCount = 3 186 | ) 187 | 188 | var triangleData = f32.Bytes(binary.LittleEndian, 189 | 0.0, 0.4, 0.0, // top left 190 | 0.0, 0.0, 0.0, // bottom left 191 | 0.4, 0.0, 0.0, // bottom right 192 | ) 193 | 194 | const vertexShader = `#version 100 195 | attribute vec4 position; 196 | void main() { 197 | gl_Position = position; 198 | }` 199 | 200 | const fragmentShader = `#version 100 201 | precision mediump float; 202 | void main() { 203 | gl_FragColor = vec4(0.8,0.8,0.8,1.0); // white 204 | }` 205 | -------------------------------------------------------------------------------- /demo/triangle2/main.go: -------------------------------------------------------------------------------- 1 | // +build darwin linux windows 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "golang.org/x/mobile/app" 13 | "golang.org/x/mobile/event/lifecycle" 14 | "golang.org/x/mobile/event/mouse" 15 | "golang.org/x/mobile/event/paint" 16 | "golang.org/x/mobile/event/size" 17 | "golang.org/x/mobile/event/touch" 18 | "golang.org/x/mobile/exp/f32" 19 | "golang.org/x/mobile/exp/gl/glutil" 20 | "golang.org/x/mobile/gl" 21 | ) 22 | 23 | type gameState struct { 24 | width int 25 | height int 26 | gl gl.Context 27 | program gl.Program 28 | bufData gl.Buffer 29 | bufIndex gl.Buffer 30 | position gl.Attrib 31 | } 32 | 33 | func main() { 34 | log.Print("main begin") 35 | 36 | slowPaint := len(os.Args) > 1 37 | log.Printf("slowPaint: %v", slowPaint) 38 | 39 | game := &gameState{} 40 | var requests int 41 | var paints int 42 | sec := time.Now().Second() 43 | 44 | app.Main(func(a app.App) { 45 | log.Print("app.Main begin") 46 | 47 | LOOP: 48 | for e := range a.Events() { 49 | switch t := a.Filter(e).(type) { 50 | case lifecycle.Event: 51 | log.Printf("Lifecycle: %v", t) 52 | 53 | if t.From > t.To && t.To == lifecycle.StageDead { 54 | log.Printf("lifecycle down to dead") 55 | break LOOP 56 | } 57 | 58 | if t.Crosses(lifecycle.StageAlive) == lifecycle.CrossOff { 59 | log.Printf("lifecycle cross down alive") 60 | break LOOP 61 | } 62 | 63 | switch t.Crosses(lifecycle.StageVisible) { 64 | case lifecycle.CrossOn: 65 | glc, isGL := t.DrawContext.(gl.Context) 66 | if !isGL { 67 | log.Printf("Lifecycle: visible: bad GL context") 68 | continue LOOP 69 | } 70 | game.start(glc) 71 | a.Send(paint.Event{}) // start drawing 72 | case lifecycle.CrossOff: 73 | game.stop() 74 | } 75 | 76 | case paint.Event: 77 | if t.External || game.gl == nil { 78 | // As we are actively painting as fast as 79 | // we can (usually 60 FPS), skip any paint 80 | // events sent by the system. 81 | continue 82 | } 83 | 84 | requests++ // events 85 | 86 | if now := time.Now().Second(); now != sec { 87 | log.Printf("requests=%d paints=%d", requests, paints) 88 | requests = 0 89 | paints = 0 90 | sec = now 91 | } 92 | 93 | //if !slowPaint || frames == 0 { 94 | paints++ // draws 95 | game.paint() 96 | a.Publish() 97 | //} 98 | 99 | if slowPaint { 100 | time.Sleep(250 * time.Millisecond) 101 | } 102 | 103 | // we request next paint event 104 | // in order to draw as fast as possible 105 | a.Send(paint.Event{}) 106 | case mouse.Event: 107 | game.input(t.X, t.Y) 108 | case touch.Event: 109 | game.input(t.X, t.Y) 110 | case size.Event: 111 | game.resize(t.WidthPx, t.HeightPx) 112 | } 113 | } 114 | 115 | log.Print("app.Main end") 116 | }) 117 | 118 | log.Print("main end") 119 | } 120 | 121 | func (game *gameState) resize(w, h int) { 122 | if game.width != w || game.height != h { 123 | log.Printf("resize: %d,%d", w, h) 124 | } 125 | game.width = w 126 | game.height = h 127 | } 128 | 129 | func (game *gameState) input(x, y float32) { 130 | log.Printf("input: %f,%f (%d x %d)", x, y, game.width, game.height) 131 | } 132 | 133 | func (game *gameState) start(glc gl.Context) { 134 | log.Printf("start") 135 | 136 | var err error 137 | game.program, err = glutil.CreateProgram(glc, vertexShader, fragmentShader) 138 | if err != nil { 139 | log.Printf("start: error creating GL program: %v", err) 140 | return 141 | } 142 | 143 | log.Printf("start: shader compiled") 144 | 145 | game.bufData = glc.CreateBuffer() 146 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufData) 147 | glc.BufferData(gl.ARRAY_BUFFER, triangleData, gl.STATIC_DRAW) 148 | 149 | game.bufIndex = glc.CreateBuffer() 150 | glc.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, game.bufIndex) 151 | glc.BufferData(gl.ELEMENT_ARRAY_BUFFER, triangleIndex, gl.STATIC_DRAW) 152 | 153 | game.position = glc.GetAttribLocation(game.program, "position") 154 | 155 | game.gl = glc 156 | } 157 | 158 | func (game *gameState) stop() { 159 | log.Printf("stop") 160 | 161 | glc := game.gl // shortcut 162 | 163 | glc.DeleteProgram(game.program) 164 | glc.DeleteBuffer(game.bufData) 165 | glc.DeleteBuffer(game.bufIndex) 166 | 167 | game.gl = nil 168 | } 169 | 170 | func (game *gameState) paint() { 171 | glc := game.gl // shortcut 172 | 173 | glc.ClearColor(.5, .5, .5, 1) // gray background 174 | glc.Clear(gl.COLOR_BUFFER_BIT) 175 | 176 | glc.UseProgram(game.program) 177 | glc.EnableVertexAttribArray(game.position) 178 | 179 | glc.BindBuffer(gl.ARRAY_BUFFER, game.bufData) 180 | glc.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, game.bufIndex) 181 | 182 | strideSize := 3 * 4 183 | offset := 0 184 | glc.VertexAttribPointer(game.position, coordsPerVertex, gl.FLOAT, false, strideSize, offset) 185 | 186 | //glc.DrawArrays(gl.TRIANGLES, 0, vertexCount) 187 | elemFirst := 0 188 | elemCount := vertexCount 189 | elemType := gl.Enum(gl.UNSIGNED_INT) 190 | elemSize := 4 191 | glc.DrawElements(gl.TRIANGLES, elemCount, elemType, elemFirst*elemSize) 192 | 193 | glc.DisableVertexAttribArray(game.position) 194 | } 195 | 196 | const ( 197 | coordsPerVertex = 3 198 | vertexCount = 3 199 | ) 200 | 201 | var triangleData = f32.Bytes(binary.LittleEndian, 202 | 0.0, 0.4, 0.0, // top left 203 | 0.0, 0.0, 0.0, // bottom left 204 | 0.4, 0.0, 0.0, // bottom right 205 | ) 206 | 207 | var triangleIndex = intsToBytes([]uint32{ 208 | 0, 1, 2, 209 | }) 210 | 211 | func intsToBytes(s []uint32) []byte { 212 | buf := new(bytes.Buffer) 213 | binary.Write(buf, binary.LittleEndian, s) 214 | b := buf.Bytes() 215 | log.Printf("intsToBytes: ints=%d bytes=%d: %v", len(s), len(b), b) 216 | return b 217 | } 218 | 219 | const vertexShader = `#version 100 220 | attribute vec4 position; 221 | void main() { 222 | gl_Position = position; 223 | }` 224 | 225 | const fragmentShader = `#version 100 226 | precision mediump float; 227 | void main() { 228 | gl_FragColor = vec4(0.8,0.8,0.8,1.0); // white 229 | }` 230 | -------------------------------------------------------------------------------- /docs/adb_logcat.md: -------------------------------------------------------------------------------- 1 | 2 | To view the log output run: 3 | 4 | adb logcat GoLog:I *:S 5 | 6 | Source: 7 | 8 | https://github.com/golang/mobile/blob/master/internal/mobileinit/mobileinit_android.go 9 | -------------------------------------------------------------------------------- /docs/refs.md: -------------------------------------------------------------------------------- 1 | 2 | https://github.com/shibukawa/nanogui-go - minimalistic cross-platform widget library for OpenGL 3 | 4 | https://github.com/tbogdala/eweygewey - OpenGL immediate-mode GUI library 5 | 6 | https://pt.slideshare.net/takuyaueda967/go-for-mobile-games 7 | 8 | https://www.slideshare.net/takuyaueda967/mobile-apps-by-pure-go-with-reverse-binding 9 | 10 | https://github.com/200sc/klangsynthese - audio 11 | 12 | https://github.com/faiface/beep - brings sound to any Go program 13 | 14 | https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Text_Rendering_02 15 | -------------------------------------------------------------------------------- /docs/vec4.md: -------------------------------------------------------------------------------- 1 | 2 | The 4th component is automatically expanded to 1.0 when it is absent. 3 | 4 | That is to say, if you pass a 3-dimensional vertex attribute pointer to a 4-dimensional vector, GL will fill-in W with 1.0 for you. I always go with this route, it avoids having to explicitly write vec4 (...) when doing matrix multiplication on the position and it also avoids wasting memory storing the 4th component. 5 | 6 | This works for 2D coordinates too, by the way. A 2D coordinate passed to a vec4 attribute becomes vec4 (x, y, 0.0, 1.0). The general rule is this: all missing components are replaced with 0.0 except for W, which is replaced with 1.0. 7 | 8 | https://stackoverflow.com/questions/18935203/shader-position-vec4-or-vec3 9 | -------------------------------------------------------------------------------- /future/future.go: -------------------------------------------------------------------------------- 1 | package future 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // FuelRechargeRate is number of units recharged per second. 8 | const FuelRechargeRate = float32(1.0 / 3.0) // 1 unit every 3 seconds 9 | 10 | // Fuel calculates new value after elap delta time interval. 0.0 to 10.0 11 | func Fuel(initial float32, elap time.Duration) float32 { 12 | fuel := initial + FuelRechargeRate*float32(elap)/float32(time.Second) 13 | if fuel > 10 { 14 | fuel = 10 15 | } 16 | return fuel 17 | } 18 | 19 | // CannonX calculates new value after elap delta time interval. 0.0 to 1.0 20 | func CannonX(initial float32, rate float32, elap time.Duration) (float32, float32) { 21 | x := initial + rate*float32(elap)/float32(time.Second) 22 | switch { 23 | case x < 0: 24 | x = -x 25 | rate = -rate 26 | case x > 1: 27 | x = 2 - x 28 | rate = -rate 29 | } 30 | return x, rate 31 | } 32 | 33 | // MissileY calculates new value after elap delta time interval. 0.0 to 1.0 34 | func MissileY(initial float32, rate float32, elap time.Duration) float32 { 35 | y := initial + rate*float32(elap)/float32(time.Second) 36 | if y > 1 { 37 | y = 1 38 | } 39 | return y 40 | } 41 | -------------------------------------------------------------------------------- /future/future_test.go: -------------------------------------------------------------------------------- 1 | package future 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestMissileY(t *testing.T) { 9 | missileY(t, .2, .5, 500*time.Millisecond, .45) 10 | missileY(t, .1, .5, time.Second, .6) 11 | } 12 | 13 | func TestFuel(t *testing.T) { 14 | fuel(t, 1, 3*time.Second, 2.0) 15 | } 16 | 17 | func TestCannonX(t *testing.T) { 18 | cannonX(t, .1, .5, time.Second, .6) 19 | } 20 | 21 | func cannonX(t *testing.T, initial, rate float32, elap time.Duration, expected float32) { 22 | x, _ := CannonX(initial, rate, elap) 23 | if x != expected { 24 | t.Errorf("cannonX: initial=%v rate=%v elap=%v expected=%v result=%v", initial, rate, elap, expected, x) 25 | } 26 | } 27 | 28 | func missileY(t *testing.T, initial, rate float32, elap time.Duration, expected float32) { 29 | y := MissileY(initial, rate, elap) 30 | if y != expected { 31 | t.Errorf("missileY: initial=%v rate=%v elap=%v expected=%v result=%v", initial, rate, elap, expected, y) 32 | } 33 | } 34 | 35 | func fuel(t *testing.T, initial float32, elap time.Duration, expected float32) { 36 | f := Fuel(initial, elap) 37 | if f != expected { 38 | t.Errorf("fuel: initial=%v elap=%v expected=%v result=%v", initial, elap, expected, f) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/udhos/fugo 2 | 3 | require ( 4 | github.com/faiface/beep v0.0.0-20181006150002-186a1b19424c 5 | github.com/hajimehoshi/oto v0.2.1 // indirect 6 | github.com/pkg/errors v0.8.0 // indirect 7 | github.com/udhos/goglmath v0.0.0-20170908172012-3ea761db4dc8 8 | github.com/udhos/pixfont v0.0.0-20170821042751-cf49ba5e073f 9 | golang.org/x/exp v0.0.0-20181210123644-7d6377eee41f // indirect 10 | golang.org/x/mobile v0.0.0-20181130133120-ca3c58166ed8 11 | golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6 12 | ) 13 | 14 | go 1.13 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/faiface/beep v0.0.0-20181006150002-186a1b19424c h1:zpnxPI/dTQVcu40VQpB74yZZ3I9NfwBh6DkOTpxv0Cs= 2 | github.com/faiface/beep v0.0.0-20181006150002-186a1b19424c/go.mod h1:A22Xnws4HqzY9DZEtQCbYRbS1858XfgH6SCFUeRiXDI= 3 | github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw= 4 | github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 5 | github.com/gopherjs/gopherwasm v1.0.0 h1:32nge/RlujS1Im4HNCJPp0NbBOAeBXFuT1KonUuLl+Y= 6 | github.com/gopherjs/gopherwasm v1.0.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI= 7 | github.com/hajimehoshi/oto v0.2.1 h1:8mn8yNgLE/irztYCw8iBaGMb2oxXwaNqDh85j9fk+X8= 8 | github.com/hajimehoshi/oto v0.2.1/go.mod h1:0ZepxT+2KLDrCm1gdkKBCQCxr+8fgQqoh0I7g+kr040= 9 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 10 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/udhos/goglmath v0.0.0-20170908172012-3ea761db4dc8 h1:bbiz7mVrrNIM63xgmgmne47zkY3fn5KNM3d4UlwmNxI= 12 | github.com/udhos/goglmath v0.0.0-20170908172012-3ea761db4dc8/go.mod h1:itH+IGsTRBPheeUxT8qx6eStgB5CHnPsExK0W/oPd4M= 13 | github.com/udhos/pixfont v0.0.0-20170821042751-cf49ba5e073f h1:9Uoi5ktIB8K/z+GpCKX0IBKgkxQFkl1LAaQ5jHz2HVQ= 14 | github.com/udhos/pixfont v0.0.0-20170821042751-cf49ba5e073f/go.mod h1:bLPNGvgDJgtArKyK9JvWJ42zGHMKB38UxUZGWa/sRHY= 15 | golang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 16 | golang.org/x/exp v0.0.0-20181210123644-7d6377eee41f h1:wJ3O7VtAmBlW5LFzggOI2U6CBWIkG+/IYf4Q/VGJdaA= 17 | golang.org/x/exp v0.0.0-20181210123644-7d6377eee41f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 18 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI= 19 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 20 | golang.org/x/mobile v0.0.0-20180806140643-507816974b79/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 21 | golang.org/x/mobile v0.0.0-20181130133120-ca3c58166ed8 h1:Q/I0fOWMTCcD88KusB5RVd4ziMZD/oEXE0zPgTV43r8= 22 | golang.org/x/mobile v0.0.0-20181130133120-ca3c58166ed8/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 23 | golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6 h1:gT0Y6H7hbVPUtvtk0YGxMXPgN+p8fYlqWkgJeUCZcaQ= 24 | golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 25 | golang.org/x/sys v0.0.0-20180806082429-34b17bdb4300 h1:eJa+6+7jje7fOYUrLnwKNR9kcpvLANj1Asw0Ou1pBiI= 26 | golang.org/x/sys v0.0.0-20180806082429-34b17bdb4300/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 27 | -------------------------------------------------------------------------------- /msg/msg.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Update message is sent from server do client. 8 | type Update struct { 9 | Fuel float32 10 | Interval time.Duration // notify client about update interval 11 | WorldMissiles []*Missile 12 | Cannons []*Cannon 13 | Team int // notify player about his team 14 | Scores [2]int 15 | FireSound bool 16 | } 17 | 18 | const ( 19 | // ButtonFire ID 20 | ButtonFire = 0 21 | // ButtonTurn ID 22 | ButtonTurn = 1 23 | ) 24 | 25 | // Button message is sent from client to server. 26 | type Button struct { 27 | ID int 28 | } 29 | 30 | // Missile is issued by cannons. 31 | type Missile struct { 32 | ID int 33 | CoordX float32 34 | CoordY float32 35 | Speed float32 36 | Team int 37 | Start time.Time 38 | } 39 | 40 | // Cannon belongs to player. 41 | type Cannon struct { 42 | ID int 43 | Start time.Time 44 | CoordX float32 45 | Speed float32 46 | Team int 47 | Player bool // belongs to player 48 | Life float32 49 | } 50 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version=`grep 'const Version =' version/version.go | awk '{ print $4 }'` 4 | v=`eval echo $version` 5 | 6 | gen_release_md() { 7 | local i=$1 8 | cat <<__EOF__ 9 | # fugo invader release 10 | 11 | Home: https://github.com/udhos/fugo 12 | 13 | $ tar xf $i.tar.gz 14 | $ cd $i 15 | 16 | Tarball contents: 17 | 18 | - arena: Server binary for desktop Linux 19 | - invader: Client binary for desktop Linux 20 | - invader.apk: Client package for Android 21 | - assets: Game assets, required to run the Arena server 22 | 23 | Run the server: 24 | 25 | $ arena 26 | 27 | Run the desktop Linux client: 28 | 29 | $ invader 30 | __EOF__ 31 | } 32 | 33 | dir=fugo-invader-$v 34 | mkdir $dir 35 | 36 | cp ~/go/bin/invader $dir 37 | cp ~/go/bin/arena $dir 38 | cp -a demo/invader/assets $dir 39 | cp invader.apk $dir 40 | cp LICENSE $dir 41 | 42 | gen_release_md $dir > $dir/RELEASE.md 43 | 44 | tar czf $dir.tar.gz $dir 45 | rm -r $dir 46 | echo $dir.tar.gz 47 | 48 | -------------------------------------------------------------------------------- /trace/trace.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | //"log" 7 | ) 8 | 9 | // Trace sends log to UDP socket. 10 | type Trace struct { 11 | conn net.Conn 12 | } 13 | 14 | // New creates new Trace. 15 | func New(server string) (*Trace, error) { 16 | conn, errDial := net.Dial("udp", server) 17 | if errDial != nil { 18 | return nil, errDial 19 | } 20 | t := &Trace{conn: conn} 21 | return t, nil 22 | } 23 | 24 | // Printf writes log to Trace. 25 | func (t *Trace) Printf(format string, v ...interface{}) { 26 | msg := fmt.Sprintf(format, v...) 27 | //log.Printf("trace.Printf: " + msg) 28 | t.conn.Write([]byte(msg)) 29 | } 30 | 31 | // Write writes log to Trace. 32 | func (t *Trace) Write(b []byte) (int, error) { 33 | return t.conn.Write(b) 34 | } 35 | -------------------------------------------------------------------------------- /unit/unit.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | const ( 8 | // ScaleCannon cannon scale 9 | ScaleCannon = .2 10 | // ScaleMissile missile scale 11 | ScaleMissile = .15 12 | // ScaleBrick brick scale 13 | ScaleBrick = .08 14 | ) 15 | 16 | // Rect is bounding box. 17 | type Rect struct { 18 | X1, Y1, X2, Y2 float64 19 | } 20 | 21 | // Bounding returns rectangle vertices. 22 | func (r Rect) Bounding() (float64, float64, float64, float64) { 23 | return r.X1, r.Y1, r.X2, r.Y2 24 | } 25 | 26 | // CannonBox returns bounding box. 27 | func CannonBox(gameMinX, gameMaxX, x, fieldTop, cannonBottom, cannonWidth, cannonHeight float64, up bool) Rect { 28 | cx := x*(gameMaxX-cannonWidth-gameMinX) + gameMinX 29 | var cy1, cy2 float64 30 | if up { 31 | // upward 32 | cy1 = cannonBottom 33 | cy2 = cy1 + cannonHeight 34 | } else { 35 | // downward 36 | cy2 = fieldTop 37 | cy1 = cy2 - cannonHeight 38 | } 39 | return Rect{ 40 | X1: cx, 41 | Y1: cy1, 42 | X2: cx + cannonWidth, 43 | Y2: cy2, 44 | } 45 | } 46 | 47 | // MissileBox returns bounding box. 48 | func MissileBox(gameMinX, gameMaxX, x, y, fieldTop, cannonBottom, cannonWidth, cannonHeight, missileWidth, missileHeight float64, up bool) Rect { 49 | minX := gameMinX + .5*cannonWidth - .5*missileWidth 50 | maxX := gameMaxX - .5*cannonWidth - .5*missileWidth 51 | fx := x*(maxX-minX) + minX 52 | var fy float64 53 | if up { 54 | // upward 55 | minY := cannonBottom + cannonHeight 56 | maxY := fieldTop - missileHeight 57 | fy = y*(maxY-minY) + minY 58 | } else { 59 | // downward 60 | minY := cannonBottom 61 | maxY := fieldTop - cannonHeight - missileHeight 62 | fy = y*(minY-maxY) + maxY 63 | } 64 | return Rect{ 65 | X1: fx, 66 | Y1: fy, 67 | X2: fx + missileWidth, 68 | Y2: fy + missileHeight, 69 | } 70 | } 71 | 72 | // Box has a bounding image.Rectangle. 73 | type Box interface { 74 | Bounds() image.Rectangle 75 | } 76 | 77 | // BoxSize returns the width,height of bounding rectangle. 78 | // Bounding rectangle in pixel. Resulting width,height in NDC (-1.0 to 1.0). 79 | func BoxSize(b Box, scale float64) (float64, float64) { 80 | sb := b.Bounds() 81 | sw := sb.Max.X - sb.Min.X 82 | sh := sb.Max.Y - sb.Min.Y 83 | var sdmax int 84 | if sw < sh { 85 | sdmax = sh 86 | } else { 87 | sdmax = sw 88 | } 89 | width := scale * float64(sw) / float64(sdmax) 90 | height := scale * float64(sh) / float64(sdmax) 91 | return width, height 92 | } 93 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Version is the game version. 4 | const Version = "0.3" 5 | --------------------------------------------------------------------------------