Resize game

This commit is contained in:
giongto35 2019-04-01 00:53:19 +08:00
parent e56e21f5f7
commit 2171f604c4
13 changed files with 813 additions and 78 deletions

BIN
000.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

21
main.go
View file

@ -17,26 +17,31 @@ import (
)
var webRTC *webrtc.WebRTC
var width = 800
var height = 600
var width = 256
var height = 240
func init() {
webRTC = webrtc.NewWebRTC()
director := ui.NewDirector()
director.Start("games/supermariobros.rom")
// start screenshot loop, wait for connection
go screenshotLoop(director.GetImageChannel())
}
func startGame(path string, imageChannel chan *image.RGBA) {
ui.Run([]string{path}, imageChannel)
}
func main() {
fmt.Println("http://localhost:8000")
webRTC = webrtc.NewWebRTC()
router := mux.NewRouter()
router.HandleFunc("/", getWeb).Methods("GET")
router.HandleFunc("/session", postSession).Methods("POST")
http.ListenAndServe(":8000", router)
go http.ListenAndServe(":8000", router)
// start screenshot loop, wait for connection
imageChannel := make(chan *image.RGBA, 2)
go screenshotLoop(imageChannel)
startGame("games/supermariobros.rom", imageChannel)
time.Sleep(time.Minute)
}
func getWeb(w http.ResponseWriter, r *http.Request) {

View file

@ -1,9 +0,0 @@
package main
import (
"github.com/giongto35/game-online/ui"
)
func startGame(path string) {
ui.Run(path)
}

54
ui/audio.go Normal file
View file

@ -0,0 +1,54 @@
package ui
import "github.com/gordonklaus/portaudio"
type Audio struct {
stream *portaudio.Stream
sampleRate float64
outputChannels int
channel chan float32
}
func NewAudio() *Audio {
a := Audio{}
a.channel = make(chan float32, 44100)
return &a
}
func (a *Audio) Start() error {
host, err := portaudio.DefaultHostApi()
if err != nil {
return err
}
parameters := portaudio.HighLatencyParameters(nil, host.DefaultOutputDevice)
stream, err := portaudio.OpenStream(parameters, a.Callback)
if err != nil {
return err
}
if err := stream.Start(); err != nil {
return err
}
a.stream = stream
a.sampleRate = parameters.SampleRate
a.outputChannels = parameters.Output.Channels
return nil
}
func (a *Audio) Stop() error {
return a.stream.Close()
}
func (a *Audio) Callback(out []float32) {
var output float32
for i := range out {
if i%a.outputChannels == 0 {
select {
case sample := <-a.channel:
output = sample
default:
output = 0
}
}
out[i] = output
}
}

View file

@ -3,28 +3,39 @@ package ui
import (
"image"
"log"
"time"
"github.com/fogleman/nes/nes"
"github.com/giongto35/game-online/nes"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.2/glfw"
)
type View interface {
Enter()
Exit()
GetImageChannel() chan *image.RGBA
Update(t, dt float64)
}
type Director struct {
view View
timestamp float64
window *glfw.Window
audio *Audio
view View
menuView View
timestamp float64
imageChannel chan *image.RGBA
}
func NewDirector() *Director {
func NewDirector(window *glfw.Window, audio *Audio, imageChannel chan *image.RGBA) *Director {
director := Director{}
director.window = window
director.audio = audio
director.imageChannel = imageChannel
return &director
}
func (d *Director) SetTitle(title string) {
d.window.SetTitle(title)
}
func (d *Director) SetView(view View) {
if d.view != nil {
d.view.Exit()
@ -33,12 +44,12 @@ func (d *Director) SetView(view View) {
if d.view != nil {
d.view.Enter()
}
d.timestamp = float64(time.Now().Unix())
d.timestamp = glfw.GetTime()
}
func (d *Director) Step() {
//timestamp := glfw.GetTime()
timestamp := float64(time.Now().Unix())
gl.Clear(gl.COLOR_BUFFER_BIT)
timestamp := glfw.GetTime()
dt := timestamp - d.timestamp
d.timestamp = timestamp
if d.view != nil {
@ -46,12 +57,22 @@ func (d *Director) Step() {
}
}
func (d *Director) Start(path string) {
d.PlayGame(path)
func (d *Director) Start(paths []string) {
d.menuView = NewMenuView(d, paths)
if len(paths) == 1 {
d.PlayGame(paths[0])
} else {
d.ShowMenu()
}
d.Run()
}
func (d *Director) Run() {
for !d.window.ShouldClose() {
d.Step()
d.window.SwapBuffers()
glfw.PollEvents()
}
d.SetView(nil)
}
@ -64,9 +85,9 @@ func (d *Director) PlayGame(path string) {
if err != nil {
log.Fatalln(err)
}
d.SetView(NewGameView(d, console, path, hash))
d.SetView(NewGameView(d, console, path, hash, d.imageChannel))
}
func (d *Director) GetImageChannel() chan *image.RGBA {
return d.view.GetImageChannel()
func (d *Director) ShowMenu() {
d.SetView(d.menuView)
}

160
ui/font.go Normal file
View file

@ -0,0 +1,160 @@
package ui
import (
"bytes"
"image"
"image/color"
"image/draw"
"image/png"
"log"
"strings"
)
var fontMask image.Image
func init() {
im, err := png.Decode(bytes.NewBuffer(fontData))
if err != nil {
log.Fatalln(err)
}
size := im.Bounds().Size()
mask := image.NewRGBA(im.Bounds())
for y := 0; y < size.Y; y++ {
for x := 0; x < size.X; x++ {
r, _, _, _ := im.At(x, y).RGBA()
if r > 0 {
mask.Set(x, y, color.Opaque)
}
}
}
fontMask = mask
}
func CreateGenericThumbnail(text string) image.Image {
im := image.NewRGBA(image.Rect(0, 0, 256, 240))
draw.Draw(im, im.Rect, &image.Uniform{color.Black}, image.ZP, draw.Src)
DrawCenteredText(im, text, 1, 2, color.RGBA{128, 128, 128, 255})
DrawCenteredText(im, text, 0, 0, color.White)
return im
}
func WordWrap(text string, maxLength int) []string {
var rows []string
words := strings.Fields(text)
if len(words) == 0 {
return rows
}
row := words[0]
for _, word := range words[1:] {
newRow := row + " " + word
if len(newRow) <= maxLength {
row = newRow
} else {
rows = append(rows, row)
row = word
}
}
rows = append(rows, row)
return rows
}
func DrawCenteredText(dst draw.Image, text string, dx, dy int, c color.Color) {
rows := WordWrap(text, 15)
for i, row := range rows {
x := 128 - len(row)*8
y := 120 - len(rows)*12 + i*24
DrawText(dst, x+dx, y+dy, row, c)
}
}
func DrawCharacter(dst draw.Image, x, y int, ch byte, c color.Color) {
if ch < 32 || ch > 128 {
return
}
cx := int((ch-32)%16) * 16
cy := int((ch-32)/16) * 16
r := image.Rect(x, y, x+16, y+16)
src := &image.Uniform{c}
sp := image.Pt(cx, cy)
draw.DrawMask(dst, r, src, sp, fontMask, sp, draw.Over)
}
func DrawText(dst draw.Image, x, y int, text string, c color.Color) {
for i := range text {
DrawCharacter(dst, x, y, text[i], c)
x += 16
}
}
var fontData = []byte{
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x60,
0x02, 0x03, 0x00, 0x00, 0x00, 0x8F, 0x9F, 0x44, 0x1B, 0x00, 0x00, 0x00,
0x06, 0x50, 0x4C, 0x54, 0x45, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFE, 0x6A,
0x62, 0xC8, 0x2E, 0x00, 0x00, 0x00, 0x01, 0x62, 0x4B, 0x47, 0x44, 0x00,
0x88, 0x05, 0x1D, 0x48, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73,
0x00, 0x00, 0x0E, 0xC4, 0x00, 0x00, 0x0E, 0xC4, 0x01, 0x95, 0x2B, 0x0E,
0x1B, 0x00, 0x00, 0x02, 0xD2, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA, 0xC5,
0x98, 0x49, 0x62, 0x23, 0x21, 0x0C, 0x45, 0xB5, 0xE1, 0x7E, 0xDA, 0xFC,
0xFB, 0x5F, 0xA5, 0x4B, 0xE8, 0x4B, 0x88, 0x72, 0x2A, 0x1D, 0x07, 0xB9,
0x9B, 0xC4, 0x2E, 0x8C, 0xE1, 0x19, 0x8D, 0x0C, 0x22, 0x56, 0x86, 0x5E,
0xFF, 0x98, 0x2F, 0x81, 0x78, 0x81, 0xB5, 0xC5, 0x77, 0xF2, 0x50, 0xE2,
0x9B, 0x63, 0x00, 0x90, 0x80, 0x71, 0xD5, 0xAD, 0xE6, 0x9F, 0xC5, 0x81,
0x90, 0xD9, 0x92, 0xF0, 0x2C, 0xF9, 0x6B, 0x4D, 0x00, 0x00, 0xEA, 0xFD,
0x31, 0x45, 0x40, 0x4C, 0xD2, 0xDE, 0x50, 0xC4, 0xBB, 0x4B, 0xD0, 0x02,
0xF0, 0xCF, 0x26, 0x82, 0xBA, 0xE2, 0xE0, 0xDF, 0x06, 0x00, 0x5E, 0xD8,
0x1D, 0xBB, 0x04, 0xE7, 0x00, 0x4E, 0xC5, 0x44, 0x98, 0xAD, 0xF6, 0x3F,
0x20, 0xF2, 0x17, 0x11, 0x96, 0x79, 0xBB, 0x00, 0xAE, 0x48, 0xA5, 0x11,
0xCB, 0x34, 0xAB, 0x19, 0xB1, 0x2C, 0x08, 0x69, 0x03, 0x24, 0x87, 0xD3,
0xB7, 0xEE, 0x48, 0xEA, 0xEE, 0xCA, 0x05, 0xA0, 0x7D, 0x80, 0xE7, 0x68,
0x79, 0x0C, 0x9F, 0x9F, 0xB4, 0xFE, 0x10, 0x90, 0x8E, 0x33, 0x03, 0xC9,
0x2A, 0xEE, 0x28, 0x70, 0xD7, 0x66, 0xBB, 0xBC, 0x2A, 0x5C, 0xDC, 0xE9,
0x8E, 0x01, 0x23, 0x03, 0x67, 0xD0, 0x89, 0x90, 0x41, 0x54, 0x82, 0x7C,
0x99, 0x70, 0x33, 0x23, 0x1A, 0x00, 0x48, 0x3B, 0x79, 0x0E, 0x63, 0x58,
0xB3, 0x1F, 0xE4, 0x0E, 0x70, 0xAF, 0x66, 0xA5, 0x17, 0x10, 0x8A, 0xA3,
0x00, 0xD1, 0x29, 0x95, 0x7B, 0xF3, 0xA3, 0x45, 0xEB, 0x02, 0xB8, 0x4D,
0xE7, 0x40, 0x95, 0x32, 0x6D, 0x57, 0xEE, 0x4B, 0xE0, 0xC6, 0xE0, 0x06,
0xC0, 0x15, 0x36, 0x9E, 0xB2, 0x62, 0x36, 0x37, 0xC5, 0x2D, 0xF3, 0x2E,
0x33, 0xA2, 0x26, 0x97, 0x63, 0xC0, 0xB4, 0x9E, 0xA5, 0x73, 0xBA, 0xED,
0x1C, 0x12, 0xEE, 0x6B, 0x7A, 0x02, 0x13, 0x6D, 0x00, 0x8A, 0x2B, 0x3B,
0xE4, 0x18, 0xF0, 0x46, 0x79, 0x23, 0xEA, 0xDF, 0x00, 0xF8, 0x54, 0xB5,
0x84, 0xB0, 0xFA, 0x12, 0xE7, 0x15, 0xF5, 0xF4, 0x36, 0xE5, 0xF4, 0x7A,
0x09, 0xB2, 0x39, 0xF6, 0x18, 0x80, 0x4C, 0xE2, 0xF6, 0xF4, 0x5A, 0xD5,
0xD7, 0x0A, 0xB6, 0xE8, 0x65, 0xBF, 0x67, 0x7F, 0x69, 0xEA, 0x53, 0x00,
0xA7, 0xC6, 0x0D, 0x8E, 0xAC, 0xE7, 0xEE, 0xEA, 0x13, 0x90, 0x71, 0x67,
0x22, 0xB6, 0x03, 0x80, 0x1C, 0x00, 0x44, 0x52, 0xA1, 0x32, 0x03, 0xB0,
0x12, 0x8A, 0x82, 0x0A, 0x6E, 0x02, 0x00, 0x0F, 0x22, 0x70, 0x9A, 0x54,
0x76, 0xB0, 0x30, 0x4C, 0x9C, 0x36, 0x40, 0xE9, 0xF8, 0x6A, 0xC6, 0x1B,
0x60, 0xA0, 0xA4, 0xFB, 0x2E, 0xC0, 0x80, 0xA6, 0xE2, 0x36, 0x57, 0xF6,
0x3D, 0x4F, 0xBA, 0xF2, 0xA0, 0x52, 0x07, 0x96, 0xD9, 0x19, 0x0B, 0x87,
0x80, 0xFF, 0x5F, 0xC0, 0xF4, 0x05, 0xA6, 0xAE, 0x3A, 0xC5, 0x4D, 0xB9,
0x53, 0x0C, 0x6E, 0x7F, 0x7D, 0x59, 0xEF, 0x02, 0x6C, 0x9D, 0xDD, 0x57,
0xB7, 0xB6, 0x19, 0xB8, 0x1E, 0xC4, 0xB9, 0xC4, 0xF8, 0xC7, 0x8F, 0x00,
0x64, 0x07, 0x0C, 0x37, 0xD5, 0x04, 0x8C, 0x72, 0x00, 0xE1, 0x36, 0xA0,
0x1B, 0xC0, 0x85, 0x25, 0x00, 0x1A, 0x89, 0x35, 0x96, 0xFE, 0x75, 0x08,
0x8B, 0x3E, 0x4D, 0x00, 0x33, 0xE1, 0x0C, 0xE9, 0x75, 0xC6, 0x0B, 0x37,
0xCD, 0xE5, 0xCE, 0x25, 0xE0, 0x10, 0xEC, 0x2B, 0xCC, 0x39, 0x80, 0x5B,
0x29, 0x0B, 0xD5, 0x1A, 0xBA, 0x73, 0xB1, 0x71, 0x41, 0x34, 0x96, 0xFF,
0x3C, 0x04, 0xB5, 0x03, 0x2E, 0x09, 0x36, 0x25, 0xFA, 0xF3, 0x96, 0xD2,
0xE8, 0xCA, 0x57, 0x89, 0x67, 0x13, 0xE0, 0x30, 0x92, 0xD1, 0x00, 0xC0,
0xC3, 0x11, 0x8E, 0xD1, 0x52, 0xF5, 0x95, 0x8E, 0xF4, 0xD5, 0x01, 0xBC,
0x1F, 0x50, 0x1F, 0x7C, 0xFB, 0x10, 0x80, 0x1B, 0x6D, 0xDF, 0xAE, 0xF8,
0xB6, 0x46, 0xD6, 0x2A, 0xAA, 0x37, 0x11, 0x34, 0x0E, 0xE6, 0xEC, 0xDF,
0x01, 0xE0, 0xE4, 0x06, 0x13, 0x57, 0xD6, 0x81, 0x78, 0x16, 0x11, 0x06,
0x3D, 0xD9, 0xF6, 0x1A, 0x31, 0xE2, 0x23, 0x00, 0xC4, 0xF1, 0x87, 0xAD,
0x09, 0xD0, 0x7F, 0x05, 0x58, 0x97, 0x0E, 0x3B, 0x20, 0x82, 0xAD, 0x1D,
0x60, 0x99, 0xE3, 0x66, 0xC6, 0x32, 0xED, 0xF1, 0x22, 0x4E, 0x00, 0xD2,
0x8C, 0xE7, 0x80, 0xE7, 0x78, 0xFD, 0xBA, 0xFE, 0x46, 0xC0, 0xFF, 0x02,
0xF0, 0x5D, 0x3F, 0xE6, 0xF6, 0x99, 0x60, 0xF1, 0x74, 0x7E, 0xF9, 0x28,
0x60, 0x9D, 0x01, 0xFC, 0xA2, 0x6A, 0x74, 0x00, 0xC2, 0x7C, 0xB6, 0x9C,
0x4A, 0x6C, 0x75, 0x96, 0x63, 0xC1, 0x5F, 0x74, 0xEF, 0x02, 0x58, 0xF7,
0x2F, 0xC7, 0x80, 0x0C, 0x5F, 0x8A, 0x84, 0xEA, 0xDA, 0xB6, 0xDE, 0x84,
0xCB, 0x22, 0x77, 0x35, 0x32, 0xD6, 0xF5, 0x61, 0x23, 0x40, 0x25, 0xAF,
0xFC, 0x36, 0x80, 0x75, 0x56, 0xD4, 0xE3, 0xBE, 0x48, 0xD9, 0xDD, 0x34,
0x00, 0x52, 0x89, 0xB9, 0xC3, 0xF2, 0x04, 0x7E, 0xA5, 0xAF, 0x10, 0xA1,
0xDC, 0x56, 0x7C, 0x02, 0xA0, 0x92, 0x8B, 0x06, 0xF7, 0xFF, 0xB2, 0xD2,
0x3A, 0xAF, 0x21, 0xFD, 0x88, 0xAC, 0xD5, 0x95, 0xF3, 0x0A, 0xB5, 0x0D,
0xF0, 0xEB, 0x7B, 0x83, 0x53, 0xC0, 0x1F, 0xEF, 0x0D, 0xA2, 0x4D, 0x77,
0x69, 0xB8, 0xB7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
0x42, 0x60, 0x82,
}

View file

@ -3,7 +3,9 @@ package ui
import (
"image"
"github.com/fogleman/nes/nes"
"github.com/giongto35/game-online/nes"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.2/glfw"
)
const padding = 0
@ -13,21 +15,23 @@ type GameView struct {
console *nes.Console
title string
hash string
texture uint32
record bool
frames []image.Image
imageChannel chan *image.RGBA
}
func NewGameView(director *Director, console *nes.Console, title, hash string) View {
imageChannel := make(chan *image.RGBA, 2)
return &GameView{director, console, title, hash, false, nil, imageChannel}
}
func (view *GameView) GetImageChannel() chan *image.RGBA {
return view.imageChannel
func NewGameView(director *Director, console *nes.Console, title, hash string, imageChannel chan *image.RGBA) View {
texture := createTexture()
return &GameView{director, console, title, hash, texture, false, nil, imageChannel}
}
func (view *GameView) Enter() {
gl.ClearColor(0, 0, 0, 1)
view.director.SetTitle(view.title)
view.console.SetAudioChannel(view.director.audio.channel)
view.console.SetAudioSampleRate(view.director.audio.sampleRate)
view.director.window.SetKeyCallback(view.onKey)
// load state
if err := view.console.LoadState(savePath(view.hash)); err == nil {
return
@ -44,6 +48,7 @@ func (view *GameView) Enter() {
}
func (view *GameView) Exit() {
view.director.window.SetKeyCallback(nil)
view.console.SetAudioChannel(nil)
view.console.SetAudioSampleRate(0)
// save sram
@ -59,40 +64,79 @@ func (view *GameView) Update(t, dt float64) {
if dt > 1 {
dt = 0
}
window := view.director.window
console := view.console
//if readKey(window, glfw.KeyEscape) {
//view.director.ShowMenu()
//}
//updateControllers(window, console)
view.imageChannel <- console.Buffer()
if joystickReset(glfw.Joystick1) {
view.director.ShowMenu()
}
if joystickReset(glfw.Joystick2) {
view.director.ShowMenu()
}
if readKey(window, glfw.KeyEscape) {
view.director.ShowMenu()
}
updateControllers(window, console)
console.StepSeconds(dt)
gl.BindTexture(gl.TEXTURE_2D, view.texture)
setTexture(console.Buffer())
view.imageChannel <- console.Buffer()
drawBuffer(view.director.window)
gl.BindTexture(gl.TEXTURE_2D, 0)
if view.record {
view.frames = append(view.frames, copyImage(console.Buffer()))
}
}
//func (view *GameView) onKey(window *glfw.Window,
//key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
//if action == glfw.Press {
//switch key {
//case glfw.KeyR:
//view.console.Reset()
//case glfw.KeyTab:
//if view.record {
//view.record = false
//view.frames = nil
//} else {
//view.record = true
//}
//}
//}
//}
func (view *GameView) onKey(window *glfw.Window,
key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
if action == glfw.Press {
switch key {
case glfw.KeySpace:
screenshot(view.console.Buffer())
case glfw.KeyR:
view.console.Reset()
case glfw.KeyTab:
if view.record {
view.record = false
animation(view.frames)
view.frames = nil
} else {
view.record = true
}
}
}
}
//func updateControllers(window *glfw.Window, console *nes.Console) {
//turbo := console.PPU.Frame%6 < 3
//k1 := readKeys(window, turbo)
//j1 := readJoystick(glfw.Joystick1, turbo)
//j2 := readJoystick(glfw.Joystick2, turbo)
//console.SetButtons1(combineButtons(k1, j1))
//console.SetButtons2(j2)
//}
func drawBuffer(window *glfw.Window) {
w, h := window.GetFramebufferSize()
s1 := float32(w) / 256
s2 := float32(h) / 240
f := float32(1 - padding)
var x, y float32
if s1 >= s2 {
x = f * s2 / s1
y = f
} else {
x = f
y = f * s1 / s2
}
gl.Begin(gl.QUADS)
gl.TexCoord2f(0, 1)
gl.Vertex2f(-x, -y)
gl.TexCoord2f(1, 1)
gl.Vertex2f(x, -y)
gl.TexCoord2f(1, 0)
gl.Vertex2f(x, y)
gl.TexCoord2f(0, 0)
gl.Vertex2f(-x, y)
gl.End()
}
func updateControllers(window *glfw.Window, console *nes.Console) {
turbo := console.PPU.Frame%6 < 3
k1 := readKeys(window, turbo)
j1 := readJoystick(glfw.Joystick1, turbo)
j2 := readJoystick(glfw.Joystick2, turbo)
console.SetButtons1(combineButtons(k1, j1))
console.SetButtons2(j2)
}

250
ui/menuview.go Normal file
View file

@ -0,0 +1,250 @@
package ui
import (
"path"
"strings"
"github.com/giongto35/game-online/nes"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.2/glfw"
)
const (
border = 10
margin = 10
initialDelay = 0.3
repeatDelay = 0.1
typeDelay = 0.5
)
type MenuView struct {
director *Director
paths []string
texture *Texture
nx, ny, i, j int
scroll int
t float64
buttons [8]bool
times [8]float64
typeBuffer string
typeTime float64
}
func NewMenuView(director *Director, paths []string) View {
view := MenuView{}
view.director = director
view.paths = paths
view.texture = NewTexture()
return &view
}
func (view *MenuView) checkButtons() {
window := view.director.window
k1 := readKeys(window, false)
j1 := readJoystick(glfw.Joystick1, false)
j2 := readJoystick(glfw.Joystick2, false)
buttons := combineButtons(combineButtons(j1, j2), k1)
now := glfw.GetTime()
for i := range buttons {
if buttons[i] && !view.buttons[i] {
view.times[i] = now + initialDelay
view.onPress(i)
} else if !buttons[i] && view.buttons[i] {
view.onRelease(i)
} else if buttons[i] && now >= view.times[i] {
view.times[i] = now + repeatDelay
view.onPress(i)
}
}
view.buttons = buttons
}
func (view *MenuView) onPress(index int) {
switch index {
case nes.ButtonUp:
view.j--
case nes.ButtonDown:
view.j++
case nes.ButtonLeft:
view.i--
case nes.ButtonRight:
view.i++
default:
return
}
view.t = glfw.GetTime()
}
func (view *MenuView) onRelease(index int) {
switch index {
case nes.ButtonStart:
view.onSelect()
}
}
func (view *MenuView) onSelect() {
index := view.nx*(view.j+view.scroll) + view.i
if index >= len(view.paths) {
return
}
view.director.PlayGame(view.paths[index])
}
func (view *MenuView) onChar(window *glfw.Window, char rune) {
now := glfw.GetTime()
if now > view.typeTime {
view.typeBuffer = ""
}
view.typeTime = now + typeDelay
view.typeBuffer = strings.ToLower(view.typeBuffer + string(char))
for index, p := range view.paths {
_, p = path.Split(strings.ToLower(p))
if p >= view.typeBuffer {
view.highlight(index)
return
}
}
}
func (view *MenuView) highlight(index int) {
view.scroll = index/view.nx - (view.ny-1)/2
view.clampScroll(false)
view.i = index % view.nx
view.j = (index-view.i)/view.nx - view.scroll
}
func (view *MenuView) Enter() {
gl.ClearColor(0.333, 0.333, 0.333, 1)
view.director.SetTitle("Select Game")
view.director.window.SetCharCallback(view.onChar)
}
func (view *MenuView) Exit() {
view.director.window.SetCharCallback(nil)
}
func (view *MenuView) Update(t, dt float64) {
view.checkButtons()
view.texture.Purge()
window := view.director.window
w, h := window.GetFramebufferSize()
sx := 256 + margin*2
sy := 240 + margin*2
nx := (w - border*2) / sx
ny := (h - border*2) / sy
ox := (w-nx*sx)/2 + margin
oy := (h-ny*sy)/2 + margin
if nx < 1 {
nx = 1
}
if ny < 1 {
ny = 1
}
view.nx = nx
view.ny = ny
view.clampSelection()
gl.PushMatrix()
gl.Ortho(0, float64(w), float64(h), 0, -1, 1)
view.texture.Bind()
for j := 0; j < ny; j++ {
for i := 0; i < nx; i++ {
x := float32(ox + i*sx)
y := float32(oy + j*sy)
index := nx*(j+view.scroll) + i
if index >= len(view.paths) || index < 0 {
continue
}
path := view.paths[index]
tx, ty, tw, th := view.texture.Lookup(path)
drawThumbnail(x, y, tx, ty, tw, th)
}
}
view.texture.Unbind()
if int((t-view.t)*4)%2 == 0 {
x := float32(ox + view.i*sx)
y := float32(oy + view.j*sy)
drawSelection(x, y, 8, 4)
}
gl.PopMatrix()
}
func (view *MenuView) clampSelection() {
if view.i < 0 {
view.i = view.nx - 1
}
if view.i >= view.nx {
view.i = 0
}
if view.j < 0 {
view.j = 0
view.scroll--
}
if view.j >= view.ny {
view.j = view.ny - 1
view.scroll++
}
view.clampScroll(true)
}
func (view *MenuView) clampScroll(wrap bool) {
n := len(view.paths)
rows := n / view.nx
if n%view.nx > 0 {
rows++
}
maxScroll := rows - view.ny
if view.scroll < 0 {
if wrap {
view.scroll = maxScroll
view.j = view.ny - 1
} else {
view.scroll = 0
view.j = 0
}
}
if view.scroll > maxScroll {
if wrap {
view.scroll = 0
view.j = 0
} else {
view.scroll = maxScroll
view.j = view.ny - 1
}
}
}
func drawThumbnail(x, y, tx, ty, tw, th float32) {
sx := x + 4
sy := y + 4
gl.Disable(gl.TEXTURE_2D)
gl.Color3f(0.2, 0.2, 0.2)
gl.Begin(gl.QUADS)
gl.Vertex2f(sx, sy)
gl.Vertex2f(sx+256, sy)
gl.Vertex2f(sx+256, sy+240)
gl.Vertex2f(sx, sy+240)
gl.End()
gl.Enable(gl.TEXTURE_2D)
gl.Color3f(1, 1, 1)
gl.Begin(gl.QUADS)
gl.TexCoord2f(tx, ty)
gl.Vertex2f(x, y)
gl.TexCoord2f(tx+tw, ty)
gl.Vertex2f(x+256, y)
gl.TexCoord2f(tx+tw, ty+th)
gl.Vertex2f(x+256, y+240)
gl.TexCoord2f(tx, ty+th)
gl.Vertex2f(x, y+240)
gl.End()
}
func drawSelection(x, y, p, w float32) {
gl.LineWidth(w)
gl.Begin(gl.LINE_STRIP)
gl.Vertex2f(x-p, y-p)
gl.Vertex2f(x+256+p, y-p)
gl.Vertex2f(x+256+p, y+240+p)
gl.Vertex2f(x-p, y+240+p)
gl.Vertex2f(x-p, y-p)
gl.End()
}

View file

@ -1,7 +1,13 @@
package ui
import (
"image"
"log"
"runtime"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/gordonklaus/portaudio"
)
const (
@ -19,16 +25,39 @@ func init() {
runtime.LockOSThread()
}
func Run(path string) {
func Run(paths []string, imageChannel chan *image.RGBA) {
// initialize audio
//portaudio.Initialize()
//defer portaudio.Terminate()
portaudio.Initialize()
defer portaudio.Terminate()
//audio := NewAudio()
//if err := audio.Start(); err != nil {
//log.Fatalln(err)
//}
//defer audio.Stop()
audio := NewAudio()
if err := audio.Start(); err != nil {
log.Fatalln(err)
}
defer audio.Stop()
// initialize glfw
if err := glfw.Init(); err != nil {
log.Fatalln(err)
}
defer glfw.Terminate()
// create window
glfw.WindowHint(glfw.ContextVersionMajor, 2)
glfw.WindowHint(glfw.ContextVersionMinor, 1)
window, err := glfw.CreateWindow(width*scale, height*scale, title, nil, nil)
if err != nil {
log.Fatalln(err)
}
window.MakeContextCurrent()
// initialize gl
if err := gl.Init(); err != nil {
log.Fatalln(err)
}
gl.Enable(gl.TEXTURE_2D)
// run director
director := NewDirector(window, audio, imageChannel)
director.Start(paths)
}

161
ui/texture.go Normal file
View file

@ -0,0 +1,161 @@
package ui
import (
"image"
"io"
"net/http"
"os"
"path"
"strings"
"github.com/go-gl/gl/v2.1/gl"
)
const textureSize = 4096
const textureDim = textureSize / 256
const textureCount = textureDim * textureDim
type Texture struct {
texture uint32
lookup map[string]int
reverse [textureCount]string
access [textureCount]int
counter int
ch chan string
}
func NewTexture() *Texture {
texture := createTexture()
gl.BindTexture(gl.TEXTURE_2D, texture)
gl.TexImage2D(
gl.TEXTURE_2D, 0, gl.RGBA,
textureSize, textureSize,
0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
gl.BindTexture(gl.TEXTURE_2D, 0)
t := Texture{}
t.texture = texture
t.lookup = make(map[string]int)
t.ch = make(chan string, 1024)
return &t
}
func (t *Texture) Purge() {
for {
select {
case path := <-t.ch:
delete(t.lookup, path)
default:
return
}
}
}
func (t *Texture) Bind() {
gl.BindTexture(gl.TEXTURE_2D, t.texture)
}
func (t *Texture) Unbind() {
gl.BindTexture(gl.TEXTURE_2D, 0)
}
func (t *Texture) Lookup(path string) (x, y, dx, dy float32) {
if index, ok := t.lookup[path]; ok {
return t.coord(index)
} else {
return t.coord(t.load(path))
}
}
func (t *Texture) mark(index int) {
t.counter++
t.access[index] = t.counter
}
func (t *Texture) lru() int {
minIndex := 0
minValue := t.counter + 1
for i, n := range t.access {
if n < minValue {
minIndex = i
minValue = n
}
}
return minIndex
}
func (t *Texture) coord(index int) (x, y, dx, dy float32) {
x = float32(index%textureDim) / textureDim
y = float32(index/textureDim) / textureDim
dx = 1.0 / textureDim
dy = dx * 240 / 256
return
}
func (t *Texture) load(path string) int {
index := t.lru()
delete(t.lookup, t.reverse[index])
t.mark(index)
t.lookup[path] = index
t.reverse[index] = path
x := int32((index % textureDim) * 256)
y := int32((index / textureDim) * 256)
im := copyImage(t.loadThumbnail(path))
size := im.Rect.Size()
gl.TexSubImage2D(
gl.TEXTURE_2D, 0, x, y, int32(size.X), int32(size.Y),
gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(im.Pix))
return index
}
func (t *Texture) loadThumbnail(romPath string) image.Image {
_, name := path.Split(romPath)
name = strings.TrimSuffix(name, ".nes")
name = strings.Replace(name, "_", " ", -1)
name = strings.Title(name)
im := CreateGenericThumbnail(name)
hash, err := hashFile(romPath)
if err != nil {
return im
}
filename := thumbnailPath(hash)
if _, err := os.Stat(filename); os.IsNotExist(err) {
go t.downloadThumbnail(romPath, hash)
return im
} else {
thumbnail, err := loadPNG(filename)
if err != nil {
return im
}
return thumbnail
}
}
func (t *Texture) downloadThumbnail(romPath, hash string) error {
url := thumbnailURL(hash)
filename := thumbnailPath(hash)
dir, _ := path.Split(filename)
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(file, resp.Body); err != nil {
return err
}
t.ch <- romPath
return nil
}

View file

@ -15,7 +15,7 @@ import (
"os/user"
"path"
"github.com/fogleman/nes/nes"
"github.com/giongto35/game-online/nes"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.2/glfw"
)
@ -189,6 +189,26 @@ func saveGIF(path string, frames []image.Image) error {
return gif.EncodeAll(file, &g)
}
func screenshot(im image.Image) {
for i := 0; i < 1000; i++ {
path := fmt.Sprintf("%03d.png", i)
if _, err := os.Stat(path); os.IsNotExist(err) {
savePNG(path, im)
return
}
}
}
func animation(frames []image.Image) {
for i := 0; i < 1000; i++ {
path := fmt.Sprintf("%03d.gif", i)
if _, err := os.Stat(path); os.IsNotExist(err) {
saveGIF(path, frames)
return
}
}
}
func writeSRAM(filename string, sram []byte) error {
dir, _ := path.Split(filename)
if err := os.MkdirAll(dir, 0755); err != nil {

View file

@ -8,7 +8,7 @@ import (
"path"
"strings"
"github.com/fogleman/nes/nes"
"github.com/giongto35/game-online/nes"
)
func testRom(path string) (err error) {