diff --git a/client/main.go b/client/main.go index 9228b36..021c53b 100644 --- a/client/main.go +++ b/client/main.go @@ -10,14 +10,19 @@ import ( "github.com/socialgorithm/elon-server/domain" ) -var inputs = make(chan domain.CarControlState) +var carControlChannel = make(chan domain.CarControlState) +var simulationControlChannel = make(chan int) func update(connection *websocket.Conn) { for { select { - case carControlState := <-inputs: + case carControlState := <-carControlChannel: message := fmt.Sprintf("input %f %f", carControlState.Steering, carControlState.Throttle) connection.WriteMessage(websocket.TextMessage, []byte(message)) + case simulationControl := <-simulationControlChannel: + message := fmt.Sprintf("control %d", simulationControl) + fmt.Println(message) + connection.WriteMessage(websocket.TextMessage, []byte(message)) } } } @@ -39,9 +44,9 @@ func main() { go update(connection) if test { - clientrenderer.Manual(inputs) + clientrenderer.Manual(carControlChannel, simulationControlChannel) } else { - // clientrenderer.ExternalProcess() + // externalClient.Process() } log.Println("Closing connection") diff --git a/clientrenderer/manual.go b/clientrenderer/manual.go index acc8aa3..0cc3d0d 100644 --- a/clientrenderer/manual.go +++ b/clientrenderer/manual.go @@ -5,6 +5,7 @@ import ( "time" "github.com/socialgorithm/elon-server/domain" + "github.com/socialgorithm/elon-server/simulator" "github.com/faiface/pixel" "github.com/faiface/pixel/pixelgl" @@ -12,11 +13,12 @@ import ( ) var inputs chan domain.CarControlState +var simulationControl chan int func run() { cfg := pixelgl.WindowConfig{ Title: "Elon Manual Driver - Socialgorithm", - Bounds: pixel.R(0, 0, 400, 200), + Bounds: pixel.R(0, 0, 300, 200), VSync: true, } win, err := pixelgl.NewWindow(cfg) @@ -50,6 +52,11 @@ func run() { throttle = 1 } + // Control sequences + if win.Pressed(pixelgl.KeyR) { + simulationControl <- simulator.SimulationRestart + } + carControlState := domain.CarControlState{ Steering: float64(steering), Throttle: float64(throttle), @@ -72,7 +79,8 @@ func run() { } // Manual initiates the render loop -func Manual(_inputs chan domain.CarControlState) { +func Manual(_inputs chan domain.CarControlState, _simulationControl chan int) { inputs = _inputs + simulationControl = _simulationControl pixelgl.Run(run) } diff --git a/main.go b/main.go index a36de96..ccae8d6 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "fmt" "log" "net/http" "strconv" @@ -16,14 +17,16 @@ import ( var upgrader = websocket.Upgrader{} var simulation *simulator.Simulation +var isTest = false func main() { var port = flag.String("port", "8080", "the port number to run on") var test = flag.Bool("test", true, "whether the server should run in test mode") + isTest = *test simulation = simulator.CreateSimulation(1) - if *test { - go simulation.Start(*test) + if isTest { + go simulation.Start(isTest) } log.Printf("Starting Elon Server on localhost:%s", *port) @@ -53,7 +56,7 @@ func connectionHandler(w http.ResponseWriter, r *http.Request) { switch message[0] { case "start": - go simulation.Start(false) + go simulation.Start(isTest) break case "input": steering, _ := strconv.ParseFloat(message[1], 64) @@ -64,6 +67,16 @@ func connectionHandler(w http.ResponseWriter, r *http.Request) { Throttle: throttle, } go simulation.Input(0, carControlState) + break + case "control": + signal, err := strconv.Atoi(message[1]) + if err != nil { + fmt.Printf("Received invalid signal %s\n", message[1]) + break + } + if signal == simulator.SimulationRestart { + simulation.Restart() + } } } } diff --git a/render/constants.go b/render/constants.go index c630a58..40b2304 100644 --- a/render/constants.go +++ b/render/constants.go @@ -1,14 +1,20 @@ package render -import "golang.org/x/image/colornames" +import ( + "math" + + "golang.org/x/image/colornames" +) const lineThickness = 4 const segmentLength = 15 const carWidth = 15 const carLength = 30 -const wheelOffset = 5 -const wheelLength = 5 +const wheelOffset = carLength / 7 +const wheelLength = carLength / 4 const wheelWidth = 3 +const maxWheelSteering = math.Pi / 4 +const zoom = 3.0 var bgColor = colornames.Green var roadColor = colornames.Gray diff --git a/render/draw.go b/render/draw.go index 9214ea8..f23b4c7 100644 --- a/render/draw.go +++ b/render/draw.go @@ -60,87 +60,106 @@ func drawStart(draw *imdraw.IMDraw, track domain.Track) { draw.Line(5) } -func renderCar(carState domain.CarState) *imdraw.IMDraw { +func renderCar(car domain.Car) *imdraw.IMDraw { carRender := imdraw.New(nil) // Prepare some vectors - posVector := pixel.V(carState.Position.X, carState.Position.Y) - dirVector := pixel.V(carState.Direction.X, carState.Direction.Y) - sensorUnitVector := pixel.Unit(-math.Pi / 2) + posVector := pixel.V(car.CarState.Position.X, car.CarState.Position.Y) + dirVector := pixel.V(car.CarState.Direction.X, car.CarState.Direction.Y) - // Rotation matrix for the car rendering - rotation := pixel.IM.Rotated(posVector, dirVector.Angle()+halfPi) + // Set coordinate system to the car center and rotation + rotationAngle := dirVector.Angle() + halfPi + rotation := pixel.IM.Moved(posVector).Rotated(posVector, rotationAngle) carRender.SetMatrix(rotation) + // Vectors adjusted to new coordinate system + carCenter := pixel.V(-carWidth*.5, -carLength*.5) + + // Sensor vectors + sensorUnitVector := pixel.Unit(math.Pi / 2) // unit vector pointing forward from the car + sensorCenter := carCenter.Add(pixel.V(carWidth*.5, 0)) // middle of the front of the car + // render car fill - if carState.Crashed != true { + if car.CarState.Crashed != true { carRender.Color = carColor } else { carRender.Color = carCrashedColor } carRender.Push( - pixel.V(carState.Position.X-carWidth*.5, carState.Position.Y-carLength*.5), - pixel.V(carState.Position.X+carWidth*.5, carState.Position.Y+carLength*.5), + carCenter, + pixel.V(carWidth*.5, carLength*.5), ) carRender.Rectangle(0) // render car outline carRender.Color = colornames.Black carRender.Push( - pixel.V(carState.Position.X-carWidth*.5, carState.Position.Y-carLength*.5), - pixel.V(carState.Position.X+carWidth*.5, carState.Position.Y+carLength*.5), + carCenter, + pixel.V(carWidth*.5, carLength*.5), ) carRender.Rectangle(1) // render car middle point carRender.Color = colornames.Yellow - carRender.Push( - pixel.V(carState.Position.X, carState.Position.Y), - ) + carRender.Push(sensorCenter) carRender.Circle(2, 0) // render sensors carRender.Color = colornames.Orange - for i := 0; i < len(carState.Sensors); i++ { - sensor := carState.Sensors[i] - sensorVector := sensorUnitVector.Scaled(sensor.Distance).Rotated(sensor.Angle + math.Pi) + for i := 0; i < len(car.CarState.Sensors); i++ { + sensor := car.CarState.Sensors[i] + sensorVector := sensorUnitVector.Scaled(sensor.Distance).Rotated(sensor.Angle) carRender.Push( - pixel.V(carState.Position.X, carState.Position.Y), - pixel.V(carState.Position.X+sensorVector.X, carState.Position.Y+sensorVector.Y), + sensorCenter, + sensorCenter.Add(sensorVector), ) carRender.Line(1) } - // render wheels - carRender.Color = colornames.Black - // top left - carRender.Push( - pixel.V(carState.Position.X-carWidth*.5, carState.Position.Y-carLength*.5+wheelOffset), - pixel.V(carState.Position.X-carWidth*.5, carState.Position.Y-carLength*.5+wheelOffset+wheelLength), - ) - carRender.Line(wheelWidth) - // top right - carRender.Push( - pixel.V(carState.Position.X+carWidth*.5, carState.Position.Y-carLength*.5+wheelOffset), - pixel.V(carState.Position.X+carWidth*.5, carState.Position.Y-carLength*.5+wheelOffset+wheelLength), - ) - carRender.Line(wheelWidth) - // bottom left - carRender.Push( - pixel.V(carState.Position.X-carWidth*.5, carState.Position.Y+carLength*.5-wheelOffset), - pixel.V(carState.Position.X-carWidth*.5, carState.Position.Y+carLength*.5-wheelOffset-wheelLength), - ) - carRender.Line(wheelWidth) - // bottom right - carRender.Push( - pixel.V(carState.Position.X+carWidth*.5, carState.Position.Y+carLength*.5-wheelOffset), - pixel.V(carState.Position.X+carWidth*.5, carState.Position.Y+carLength*.5-wheelOffset-wheelLength), - ) - carRender.Line(wheelWidth) + renderWheels(car, rotation, rotationAngle, carRender) + return carRender } +func renderWheels(car domain.Car, initialMatrix pixel.Matrix, rotationAngle float64, carRender *imdraw.IMDraw) { + carRender.Color = colornames.Black + + wheelPositions := [4][3]float64{ + // carWidth, carLength, wheelOffset + [3]float64{-1, -1, +1}, // top left + [3]float64{+1, -1, +1}, // top right + [3]float64{-1, +1, -1}, // bottom left + [3]float64{+1, +1, -1}, // bottom right + } + + carPosition := pixel.V(car.CarState.Position.X, car.CarState.Position.Y) + + carRender.Color = colornames.Black + + for i := 0; i < len(wheelPositions); i++ { + wheelData := wheelPositions[i] + // prepare all vectors + wheelCenter := pixel.V( + wheelData[0]*carWidth*.5, + wheelData[1]*carLength*.5+wheelData[2]*wheelOffset+wheelData[2]*wheelLength/2, + ).Add(carPosition) + lengthVec := pixel.V(0, wheelLength/2) + wheelMatrix := pixel.IM.Moved(wheelCenter).Rotated(carPosition, rotationAngle) + adjustedWheelCenter := pixel.ZV + // if i < 2 { + // // front 2 wheels - ugly, whether the wheels turn should be a param somewhere + // wheelMatrix = wheelMatrix.Rotated(wheelCenter, maxWheelSteering) + // } + carRender.SetMatrix(wheelMatrix) + carRender.Push( + adjustedWheelCenter.Add(lengthVec), + adjustedWheelCenter.Sub(lengthVec), + ) + carRender.Line(wheelWidth) + } +} + func drawLine(draw *imdraw.IMDraw, points []domain.Position, color color.RGBA, thickness float64) { for i := 0; i < len(points)-1; i++ { pointA := points[i] diff --git a/render/render.go b/render/render.go index 017ebf1..240fc81 100644 --- a/render/render.go +++ b/render/render.go @@ -48,13 +48,21 @@ func run() { for !win.Closed() { win.Clear(bgColor) + // follow top car with camera and zoom + camPos := pixel.V( + cars[0].CarState.Position.X, + cars[0].CarState.Position.Y, + ) + cam := pixel.IM.Scaled(camPos, zoom).Moved(win.Bounds().Center().Sub(camPos)) + win.SetMatrix(cam) + // redraw the track trackRender.Draw(win) // update cars if len(cars) > 0 { for i := range cars { - carRender := renderCar(cars[i].CarState) + carRender := renderCar(cars[i]) carRender.Draw(win) } } diff --git a/simulator/domain.go b/simulator/domain.go index c205cff..4f86cfa 100644 --- a/simulator/domain.go +++ b/simulator/domain.go @@ -12,3 +12,8 @@ type Simulation struct { CarsChannel chan []domain.Car Engine physics.Engine } + +const ( + // SimulationRestart Control signal to restart a simulation + SimulationRestart int = 0 +) diff --git a/simulator/simulator.go b/simulator/simulator.go index 15ff337..20bd16b 100644 --- a/simulator/simulator.go +++ b/simulator/simulator.go @@ -2,11 +2,8 @@ package simulator import ( "log" - "math" - "math/rand" "time" - "github.com/faiface/pixel" "github.com/socialgorithm/elon-server/domain" "github.com/socialgorithm/elon-server/physics" ) @@ -16,10 +13,14 @@ const ( ) var simulation Simulation +var carCount = 0 +var testMode = false +var track domain.Track // CreateSimulation creates a new simulation -func CreateSimulation(carCount int) *Simulation { - track := ReadTrack() +func CreateSimulation(_carCount int) *Simulation { + track = ReadTrack() + carCount = _carCount return &Simulation{ Track: track, CarsChannel: make(chan []domain.Car), @@ -28,7 +29,8 @@ func CreateSimulation(carCount int) *Simulation { } // Start starts the physics engine (run this in goroutine to async, don't put in the method) -func (simulation Simulation) Start(testMode bool) { +func (simulation Simulation) Start(_testMode bool) { + testMode = _testMode log.Println("Starting simulation") for { simulation.CarsChannel <- simulation.Engine.Next() @@ -36,42 +38,12 @@ func (simulation Simulation) Start(testMode bool) { } } +// Restart restarts the simulation and all the cars in it +func (simulation Simulation) Restart() { + simulation.Engine = physics.NewEngine(track, carCount) +} + // Input add an input to the simulation func (simulation Simulation) Input(carIndex int, carControlState domain.CarControlState) { simulation.Engine.SetCtrl(0, carControlState) } - -func genCars(carCount int, track domain.Track) []domain.Car { - cars := make([]domain.Car, carCount) - centre0 := pixel.V(track.Center[0].X, track.Center[0].Y) - centre1 := pixel.V(track.Center[1].X, track.Center[1].Y) - startAngle := centre1.Sub(centre0).Unit() - for i := range cars { - cars[i].CarState = domain.CarState{ - Crashed: false, - Position: track.Center[0], - Direction: domain.Position{X: startAngle.X, Y: startAngle.Y}, - Velocity: 0, - // using 4 sensors for now - Sensors: genRandomSensorData(), - } - } - - return cars -} - -func genRandomSensorData() []domain.Sensor { - const sensorCount = 4 - minSensorDistance := 10.0 - maxSensorDistance := 50.0 - sensorArc := math.Pi - var sensors [sensorCount + 1]domain.Sensor - sensorAngleIncrement := sensorArc / sensorCount - for i := 0; i <= sensorCount; i++ { - sensors[i] = domain.Sensor{ - Angle: -sensorArc/2 + sensorAngleIncrement*float64(i), - Distance: rand.Float64()*maxSensorDistance + minSensorDistance, - } - } - return sensors[0:len(sensors)] -}