Reputation: 1830
I want to make a button highlighted when selected (light blue background). To do so, I wrote the following code:
players : Input Int
players = input 1
playerSelection : Element
playerSelection = flow right [
color (buttonPressed 1) (button players.handle 1 "Solo")
, color (buttonPressed 2) (button players.handle 2 "2P")
, color (buttonPressed 3) (button players.handle 3 "3P")
, color (buttonPressed 4) (button players.handle 4 "4P")
]
buttonPressed : Int -> Color
buttonPressed p = if players.signal == p then lightBlue else white
When I run this along the rest of my code, I get this error msg:
if | players.signal == p -> lightBlue
| True -> white
Expected Type: Int
Actual Type: Signal.Signal Int
The problem is that players.signal is a Signal Int. If I replace this to a normal number, my program works as it should. However, I need a button to become light blue when selected and white again when another button is selected. Is there any way to transmute the signal to a normal integer for comparing purposes? I have tried creating an external compare method and lift the signal to that method, but the result of this method would then be a Signal Bool that I cannot use. To illustrate what I did:
buttonPressed : Int -> Color
buttonPressed p = if equals p <~ players.signal then lightBlue else white
equals : Int -> Int -> Bool
equals a b = a == b
I have tried to move the signal 'higher up' to the calling functions, but the problem remains for me. I might as well show all of the code (it's not much, I should have done this in the first place).
heartbeat = every (second/50)
data Player = One | Two | Three | Four
-- STARTING SCREEN
players : Input Int
players = input 1
beginState : Int -> Element
beginState ps = collage 600 600 [
move (0,270) (toForm (playerSelection ps))
]
playerSelection : Int -> Element
playerSelection ps = flow right [
color (buttonPressed 1 ps) (button players.handle 1 "Solo")
, color (buttonPressed 2 ps) (button players.handle 2 "2P")
, color (buttonPressed 3 ps) (button players.handle 3 "3P")
, color (buttonPressed 4 ps) (button players.handle 4 "4P")
]
buttonPressed : Int -> Int -> Color
buttonPressed p ps = if p == ps then lightBlue else white
playState : Element
playState = draw
draw : Element
draw = collage 1024 768 [
filled black (rect 1024 768)
]
endState : String -> Element
endState p = plainText (show p ++ " has won!")
startGame : Signal Element
startGame = foldp (\_ _ -> draw) (beginState <~ players.signal) Keyboard.space
main : Signal Element
main = startGame
I'll explain what my end goal is first. The game starts in a beginning screen (beginState). In this screen, the player(s) can customize:
- a nickname (textfield returning a string)
- controls (wasd, arrow keys, any keys the user defines for directions)
- amount of players
When space bar has been entered, the game starts and these parameters can no longer be changed. This can be seen in startGame. How the actual game works is of little relevance to this topic, but it's important to know that I expect the customized options from the start screen to be transferred to the game mechanics. So input keys for certain actions per player must coincide with characters chosen in the controls textfields. The number of players chosen will determine which objects get created in the game. The playState of this game is currently replaced by a holder draw
.
On to the problem:
I'm slowly learning that I cannot simply ask for the amount of players by calling players.signal
because I'd get a Signal Int instead of an Int. This means I will not be able to call, say controls.signal
, to retrieve a char to put in Keyboard.directions
(because I'll get a Signal Char instead of Char). The question is basically, how do I store and pass on information from signals like I would with variables in other programming languages?
The more I work on this code, the more I realize I don't understand how signals work. In your example (Apanatshka) I see you lift a signal to playerSelection, which expects an Int as argument instead of a Signal Int. When I try to apply this (see code higher up in this update section), I get the following error:
Type error on line 76, column 35 to 63:
beginState <~ players.signal
Expected Type: Signal.Signal Graphics.Element.Element
Actual Type: Graphics.Element.Element
I got the general idea what could be wrong. Two signals are being sent from one function and it's expecting only one. Honestly, I'd much rather see the signal propagated to a more relevant position like in beginState:
beginState : Element
beginState = collage 600 600 [
move (0,270) (toForm (playerSelection <~ players.signal))
]
But then toForm gets a Signal Element argument instead of Element, which spells problems again. I could technically put the signal all the way up in main, but I have a very bad feeling about it because players.signal
is no longer needed once the game has started and the signal's been passed on as an Int parameter.
I hope I'm clear in what I want to accomplish. And to sum up my issues quickly:
- Where would players.signal
fit in my code without propping all of my signals to main?
- How would I store and pass on information from a signal?
Upvotes: 2
Views: 219
Reputation: 5958
I think the Learn page of the Elm website has some good resources on the general questions that you're asking:
I'll try to give a short recap and add my opinion because these are just links to official docs.
First off, type errors from the Elm compiler can be confusing. Expected vs. Actual is usually given in an unintuitive order. But the type error you're getting is from giving foldp
a base value that is a Signal. The type of foldp : (a -> b -> b) -> b -> Signal a -> Signal b
shows that if b
= Signal Element
, then the result of the foldp
is Signal (Signal b)
. This is generally the problematic situation then you get into when you start using signals in your code "too soon".
I understand that you think of signals are mutable variables and just want to mention them wherever you want their current value. But as you have noted, that's basically where signals are different from mutable variables. So the general idea for a simple architecture is to combine all your inputs into one data structure:
data Input =
Players Int
| Control String
input : Signal Input
input = merge (Players <~ players.signal) (Control <~ control.signal)
You build another data structure that holds all the state your program needs and define an initialState
.
Then you define a general update function, where given a new input you can update the state.
You also create a view
function to show your state.
Then you create main
:
main = view <~ foldp update initialState input
This is basically what's describe as a common pattern in the first link. I think this answers your question How would I store and pass on information from a signal?: with foldp
.
It answers Where would players.signal
fit in my code without propping all of my signals to main? because the answer here is: you have to prop all your signals to main
. But that answer is unsatisfactory and may not be scalable, so that's why I included the second link.
The pattern of a single foldp
to hold all your state seems a little overly strict.1 But it works.. Until you start scaling it up and want some separation of concerns for different components.
So the basic idea of the Architecture page is to create components that don't interact by defining their state, input, update, view in a module. Then you main program imports the different modules, creates data structures to combine the different states/inputs/updates/views and be able to use a single foldp
again, in the end, to use in main
.
You're thinking in the right direction. The idea is to lift players.signal
out of the buttonPressed
function altogether. The general advice for Signal
s is try not to use them at all, then at the last moment ---in main
--- use them. If main
gets too complex, then refactor even if that means using signals in other functions. But try to start out with only non-signal functions.
Only use a signal in main, so to use players.signal
in buttonPressed
you need to pass it along as an argument (notice the argument is Int
, not Signal Int
):
buttonPressed : Int -> Int -> Color
buttonPressed ps p = if ps == p then lightBlue else white
You need to pass this Int
from playerSelection
to buttonPressed
, so add it as an argument there as well:
playerSelection : Int -> Element
playerSelection ps = flow right [
color (buttonPressed ps 1) (button players.handle 1 "Solo")
, color (buttonPressed ps 2) (button players.handle 2 "2P")
, color (buttonPressed ps 3) (button players.handle 3 "3P")
, color (buttonPressed ps 4) (button players.handle 4 "4P")
]
Now you can write main like so:
main = playerSelection <~ players.signal
import Graphics.Input (Input, input, button)
players : Input Int
players = input 1
playerSelection : Int -> Element
playerSelection ps = flow right [
color (buttonPressed ps 1) (button players.handle 1 "Solo")
, color (buttonPressed ps 2) (button players.handle 2 "2P")
, color (buttonPressed ps 3) (button players.handle 3 "3P")
, color (buttonPressed ps 4) (button players.handle 4 "4P")
]
buttonPressed : Int -> Int -> Color
buttonPressed ps p = if ps == p then lightBlue else white
main = playerSelection <~ players.signal
Upvotes: 2