问题
I have some existing C# code for a very, very simple RogueLike engine. It is deliberately naive in that I was trying to do the minimum amount as simply as possible. All it does is move an @ symbol around a hardcoded map using the arrow keys and System.Console:
//define the map
var map = new List<string>{
" ",
" ",
" ",
" ",
" ############################### ",
" # # ",
" # ###### # ",
" # # # # ",
" #### #### # # # ",
" # # # # # # ",
" # # # # # # ",
" #### #### ###### # ",
" # = # ",
" # = # ",
" ############################### ",
" ",
" ",
" ",
" ",
" "
};
//set initial player position on the map
var playerX = 8;
var playerY = 6;
//clear the console
Console.Clear();
//send each row of the map to the Console
map.ForEach( Console.WriteLine );
//create an empty ConsoleKeyInfo for storing the last key pressed
var keyInfo = new ConsoleKeyInfo( );
//keep processing key presses until the player wants to quit
while ( keyInfo.Key != ConsoleKey.Q ) {
//store the player's current location
var oldX = playerX;
var oldY = playerY;
//change the player's location if they pressed an arrow key
switch ( keyInfo.Key ) {
case ConsoleKey.UpArrow:
playerY--;
break;
case ConsoleKey.DownArrow:
playerY++;
break;
case ConsoleKey.LeftArrow:
playerX--;
break;
case ConsoleKey.RightArrow:
playerX++;
break;
}
//check if the square that the player is trying to move to is empty
if( map[ playerY ][ playerX ] == ' ' ) {
//ok it was empty, clear the square they were standing on before
Console.SetCursorPosition( oldX, oldY );
Console.Write( ' ' );
//now draw them at the new square
Console.SetCursorPosition( playerX, playerY );
Console.Write( '@' );
} else {
//they can't move there, change their location back to the old location
playerX = oldX;
playerY = oldY;
}
//wait for them to press a key and store it in keyInfo
keyInfo = Console.ReadKey( true );
}
I was playing around with doing it in F#, initially I was trying to write it using functional concepts, but turned out I was a bit over my head, so I did pretty much a straight port - it's not really an F# program (though it compiles and runs) it's a procedural program written in F# syntax:
open System
//define the map
let map = [ " ";
" ";
" ";
" ";
" ############################### ";
" # # ";
" # ###### # ";
" # # # # ";
" #### #### # # # ";
" # # # # # # ";
" # # # # # # ";
" #### #### ###### # ";
" # = # ";
" # = # ";
" ############################### ";
" ";
" ";
" ";
" ";
" " ]
//set initial player position on the map
let mutable playerX = 8
let mutable playerY = 6
//clear the console
Console.Clear()
//send each row of the map to the Console
map |> Seq.iter (printfn "%s")
//create an empty ConsoleKeyInfo for storing the last key pressed
let mutable keyInfo = ConsoleKeyInfo()
//keep processing key presses until the player wants to quit
while not ( keyInfo.Key = ConsoleKey.Q ) do
//store the player's current location
let mutable oldX = playerX
let mutable oldY = playerY
//change the player's location if they pressed an arrow key
if keyInfo.Key = ConsoleKey.UpArrow then
playerY <- playerY - 1
else if keyInfo.Key = ConsoleKey.DownArrow then
playerY <- playerY + 1
else if keyInfo.Key = ConsoleKey.LeftArrow then
playerX <- playerX - 1
else if keyInfo.Key = ConsoleKey.RightArrow then
playerX <- playerX + 1
//check if the square that the player is trying to move to is empty
if map.Item( playerY ).Chars( playerX ) = ' ' then
//ok it was empty, clear the square they were standing on
Console.SetCursorPosition( oldX, oldY )
Console.Write( ' ' )
//now draw them at the new square
Console.SetCursorPosition( playerX, playerY )
Console.Write( '@' )
else
//they can't move there, change their location back to the old location
playerX <- oldX
playerY <- oldY
//wait for them to press a key and store it in keyInfo
keyInfo <- Console.ReadKey( true )
So my question is, what do I need to learn in order to rewrite this more functionally, can you give me some hints, a vague overview, that kind of thing.
I'd prefer a shove in the right direction rather than just seeing some code, but if that's the easiest way for you to explain it to me then fine, but in that case can you please also explain the "why" rather the "how" of it?
回答1:
Game programming in general will test your ability to manage complexity. I find that functional programming encourages you to break problems your solving into smaller pieces.
The first thing you want to do is turn your script into a bunch of functions by separating all the different concerns. I know it sounds silly but the very act of doing this will make the code more functional (pun intended.) Your main concern is going to be state management. I used a record to manage the position state and a tuple to manage the running state. As your code gets more advanced you will need objects to manage state cleanly.
Try adding more to this game and keep breaking the functions apart as they grow. Eventually you will need objects to manage all the functions.
On a game programming note don't change state to something else and then change it back if it fails some test. You want minimal state change. So for instance below I calculate the newPosition
and then only change the playerPosition
if this future position passes.
open System
// use a third party vector class for 2D and 3D positions
// or write your own for pratice
type Pos = {x: int; y: int}
with
static member (+) (a, b) =
{x = a.x + b.x; y = a.y + b.y}
let drawBoard map =
//clear the console
Console.Clear()
//send each row of the map to the Console
map |> List.iter (printfn "%s")
let movePlayer (keyInfo : ConsoleKeyInfo) =
match keyInfo.Key with
| ConsoleKey.UpArrow -> {x = 0; y = -1}
| ConsoleKey.DownArrow -> {x = 0; y = 1}
| ConsoleKey.LeftArrow -> {x = -1; y = 0}
| ConsoleKey.RightArrow -> {x = 1; y = 0}
| _ -> {x = 0; y = 0}
let validPosition (map:string list) position =
map.Item(position.y).Chars(position.x) = ' '
//clear the square player was standing on
let clearPlayer position =
Console.SetCursorPosition(position.x, position.y)
Console.Write( ' ' )
//draw the square player is standing on
let drawPlayer position =
Console.SetCursorPosition(position.x, position.y)
Console.Write( '@' )
let takeTurn map playerPosition =
let keyInfo = Console.ReadKey true
// check to see if player wants to keep playing
let keepPlaying = keyInfo.Key <> ConsoleKey.Q
// get player movement from user input
let movement = movePlayer keyInfo
// calculate the players new position
let newPosition = playerPosition + movement
// check for valid move
let validMove = newPosition |> validPosition map
// update drawing if move was valid
if validMove then
clearPlayer playerPosition
drawPlayer newPosition
// return state
if validMove then
keepPlaying, newPosition
else
keepPlaying, playerPosition
// main game loop
let rec gameRun map playerPosition =
let keepPlaying, newPosition = playerPosition |> takeTurn map
if keepPlaying then
gameRun map newPosition
// setup game
let startGame map playerPosition =
drawBoard map
drawPlayer playerPosition
gameRun map playerPosition
//define the map
let map = [ " ";
" ";
" ";
" ";
" ############################### ";
" # # ";
" # ###### # ";
" # # # # ";
" #### #### # # # ";
" # # # # # # ";
" # # # # # # ";
" #### #### ###### # ";
" # = # ";
" # = # ";
" ############################### ";
" ";
" ";
" ";
" ";
" " ]
//initial player position on the map
let playerPosition = {x = 8; y = 6}
startGame map playerPosition
回答2:
That's a nice little game :-). In functional programming, you'd want to avoid using mutable state (as others pointed out) and you'd also want to write the core of your game as a function that doesn't have any side-effects (e.g. reading from console and writing).
The key part of the game is the function that controls the position. You could refactor your code to have a function with the type signature:
val getNextPosition : (int * int) -> ConsoleKey -> option<int * int>
The function returns None
if the game should quit. Otherwise it returns Some(posX, posY)
where posX
and posY
are your new locations for the @
symbol. By doing the change, you get a nice functional core and the function getNextPosition
is also easy to test (because it always returns the same result for the same inputs).
To use the function, the best option is to write the looping using recursion. The structure of the main function would look like this:
let rec playing pos =
match getNextPosition pos (Console.ReadKey()) with
| None -> () // Quit the game
| Some(newPos) ->
// This function redraws the screen (this is a side-effect,
// but it is localized to a single function)
redrawScreen pos newPos
playing newPos
回答3:
Being a game, and using the Console, there is state and side-effects here which are inherent. But the key thing you'll want to do is eliminate those mutables. Using a recursive loop instead of a while loop will help you do that since then you can pass your state as arguments to each recursive call. Other than that, the main thing I can see to take advantage of F# features here is using pattern matching instead of if/then statements and switches, though that would be a mainly aesthetic improvement.
回答4:
I'll try and avoid being overly specific - if I end up going too far in the other direction and this is too vague, let me know and I'll try improve it a little.
When making a functional program that has some sort of state, the basic mechanism you want to implement is something like:
(currentState, input) => newState
Then you can write a small wrapper around that to handle fetching input and drawing output.
来源:https://stackoverflow.com/questions/4495993/very-simple-roguelike-in-f-making-it-more-functional