とことんF#よぷよ! 第 63 回 CLR/H 勉強会で、F#とXNAを題材に発表します。
F# + XNAでとことんF#よぷよ!してみました
「ダークソウル」で心が折れそうな日々を送っている今日このごろですが、みなさんはいかがお過ごしでしょうか。
F# + XNAで「とことんぷよぷよ」っぽいものを実装してみました(はじめてのXNAゲームプログラミング)。
ただし、意図的、あるいは意図せずに元ネタのそれの仕様とは異なる場合があります。
動画の終盤、PAUSEをしてチート機能を使うことで19連鎖のデモをしています。
第 63 回 CLR/H 勉強会で、F#とXNAを題材にお話します。
日時 : 2011/10/15 (土) 13:30〜18:00(開場 13:00)
場所 : マイクロソフト北海道支店 セミナールーム (札幌市中央区北 5 条西 2 丁目 5 JR タワーオフィスプラザさっぽろ 20F)
http://www.microsoft.com/ja-jp/mscorp/branch/hokkaido.aspx
参加費:500 円(会場費やお菓子代に使用させて頂きます)
【タイトル】
とことんF#よぷよ! - F# + XNAによるゲームプログラミング入門 -
【概要】
みんな大好き、落ち物パズルゲーム「ぷよぷよ」は今年で 20 周年を迎えました。
ぷよぷよ風落ち物パズルゲームの作り方と、関数型パラダイムを中心としたマルチパラダイム言語 F# による
XNA でのゲーム開発の基本についてお話します。
「ぷよぷよ風落ちゲーを 500 行程度で実装できる。そう、F# + XNA ならね!」
【スピーカー】
わたし
CLR/H 公式サイト
http://clr-h.jp/
また、マイクロソフトのエバンジェリストで、著作に「The root of .NET Framework」や「実践 F# 関数型プログラミング入門」などがある
荒井省三さんが、「DLR + ASync + アルファ」というタイトルで濃いセッションをしてくださいます。ぜひお越しください。
事前に「とことんF#よぷよ!」のコードを晒してみる
ということで、F#でXNAなコードを勉強会前に晒してしまう大盤振る舞い(!?)
コードは決してうつくしくはありませんが、興味のある方はごらんください。
PuyoPuyo.fs
namespace PuyoPuyoLibrary open System type PuyoColors = | n = 0x00 | r = 0x01 | y = 0x02 | p = 0x03 | g = 0x04 | b = 0x05 type Union = | None = 0b0000 | Top = 0b0001 | Left = 0b0010 | Bottom = 0b0100 | Right = 0b1000 type puyoObj = { position : int * int; pattern : PuyoColors[][]; color1: PuyoColors; color2: PuyoColors; hidden: bool; upside : bool} type PuyoState = { pw : int; ph : int; width : int; height : int gameover : bool; pause : bool ;cheat : bool totalScore : decimal; highScore : decimal; maxLevel : int scoreBase : int; magnifyingPower : int rensa : int; union : int; colors : int; erased : int current : puyoObj; next : puyoObj array field : PuyoColors[,]; checkField : bool[,] falling : bool; allclear : bool etarget : (int * int * PuyoColors) list} module PuyoPuyo = let patterns x y = [| [| enum 0; x; enum 0; |] [| enum 0; y; enum 0; |] [| enum 0; enum 0; enum 0; |] |] let none : PuyoColors [][] = [| [| |] |] let clearCheckField ps = { ps with checkField = Array2D.create ps.width ps.height false } let getLevel erased = erased / 40 |> fun x -> if x >= 999 then 999 else x + 1 let create erased = let rand = new Random(System.DateTime.Now.Millisecond) let create' = fun s e -> enum (rand.Next(s, e)), enum (rand.Next(s, e)) fun () -> (if getLevel erased < 3 then (1,4) ||> create' elif getLevel erased < 5 then (1,5) ||> create' else (1,6) ||> create') ||> fun x y -> { position = (1, 0); pattern = patterns x y; color1 = x; color2 = y; hidden = false; upside = true } let getPuyoObj = let queue = new System.Collections.Generic.Queue<puyoObj>() fun erased -> queue.Count |> function | 0 -> seq {1..3} |> Seq.iter (fun x -> queue.Enqueue ( () |> create erased)) queue.Dequeue(),queue.ToArray() | _ -> queue.Enqueue(() |> create erased) queue.Dequeue(),queue.ToArray() let convert (source : PuyoColors [][]) = (Array.length source, source.GetLength(0)) ||> fun row col -> Array2D.create row col PuyoColors.n |> fun array -> if source = none then array else seq { for i in 0..row - 1 do for j in 0..col - 1 do yield i,j } |> Seq.iter (fun (i,j) -> array.[j,i] <- source.[i].[j]) array type Direction = | Right | Left | Down let move ps direction = ps.current.position ||> fun x y -> direction |> function | Right -> { ps.current with position = x + 1, y } | Left -> { ps.current with position = x - 1, y } | Down -> { ps.current with position = x , y + 1 } let descend ps = ps.current.position ||> fun x y -> if y + 1 < ps.height then { ps.current with position = x, y + 1 } else ps.current let rotate puyo action = convert puyo.pattern |> fun pattern -> let len = Array2D.length1 pattern seq { for i in 0..len - 1 do for j in 0..len - 1 do yield i,j,len,pattern} |> Seq.iter action puyo let avoidance ps exchange (c1,c2) = let (|Insert|_|) c1 c2 ps = ps.current.position ||> fun x y -> let judge c f g= if (c <> PuyoColors.n && (x < 0 || (x >= 0 && (ps.field.[x,y] <> PuyoColors.n || ps.field.[x,y+1] <> PuyoColors.n)))) && (c <> PuyoColors.n && (x+2 > ps.width - 1 || (x+2 <= ps.width - 1 && (ps.field.[x+2,y] <> PuyoColors.n || ps.field.[x+2,y+1] <> PuyoColors.n)))) then f() else g() judge c1 (fun () -> Some ps.current) (fun () -> judge c2 (fun () -> Some ps.current) (fun () -> None)) let (|CollideLeft|_|) c1 c2 ps = ps.current.position ||> fun x y -> if (c1 <> PuyoColors.n || c2 <> PuyoColors.n) && (x < 0 || (x >= 0 && (ps.field.[x,y] <> PuyoColors.n || ps.field.[x,y+1] <> PuyoColors.n ))) then Some ps.current else None let (|CollideRight|_|) c1 c2 ps = ps.current.position ||> fun x y -> if (c1 <> PuyoColors.n || c2 <> PuyoColors.n) && (x+2 > ps.width - 1 || (x+2 <= ps.width - 1 && (ps.field.[x+2,y] <> PuyoColors.n || ps.field.[x+2,y+1] <> PuyoColors.n))) then Some ps.current else None ps |> function | Insert c1 c2 puyo -> puyo | CollideLeft c1 c2 puyo -> move ps Right |> rotate <| exchange | CollideRight c1 c2 puyo -> move ps Left |> rotate <| exchange | _ -> ps.current |> rotate <| exchange let target ps = ps.current.pattern.[2].[1], ps.current.pattern.[0].[1] let rotateR ps = if ps.current.pattern = none then ps.current else (fun (i,j,len,pattern:PuyoColors[,]) -> ps.current.pattern.[i].[j] <- pattern.[i,len - 1 - j]) |> avoidance ps <| target ps let rotateL ps = if ps.current.pattern = none then ps.current else (fun (i,j,len,pattern:PuyoColors[,]) -> ps.current.pattern.[j].[i] <- pattern.[len - 1 - j,i]) |> avoidance ps <| target ps let createState pw ph width height erased = let current,next = getPuyoObj erased { pw = pw; ph = ph; width = width; height = height; gameover = false; pause = false; cheat = false; totalScore = decimal 0; highScore = decimal 0; maxLevel = 0; scoreBase = 0; magnifyingPower = 0; rensa = 0; union = 0; colors = 0; erased = 0; current = current; next = next; field = Array2D.create width height PuyoColors.n checkField = Array2D.create width height false; falling = false; allclear = false; etarget = List.empty } let nextPuyo ps = if not ps.cheat then getPuyoObj ps.erased ||> fun current next -> {ps with current= current; next = next} else { ps with current = { position = (1, 0); pattern = patterns PuyoColors.p PuyoColors.y; color1 = PuyoColors.p; color2 = PuyoColors.y; hidden = false; upside = true }; next = ps.next } let reset ps = nextPuyo { ps with erased = 0 } |> fun ps -> nextPuyo ps |> fun ps -> nextPuyo ps |> fun ps -> { ps with field = Array2D.create ps.width ps.height PuyoColors.n; checkField = Array2D.create ps.width ps.height false; etarget = List.empty gameover = false; pause = false; cheat = false; totalScore = decimal 0; maxLevel = ps.maxLevel; highScore = ps.highScore; scoreBase = 0; magnifyingPower = 0; rensa = 0; union = 0; colors = 0; allclear = false } let cheat ps = Array2D.create ps.width ps.height PuyoColors.n |> fun newfield -> let dic = dict[0,[0;0;0;2;1;1];1,[1;2;0;3;2;3];2,[3;2;3;2;1;1];3,[1;2;3;2;1;3];4,[2;3;2;1;2;3];5,[3;2;1;2;1;3];6,[2;3;2;1;2;1];7,[2;3;2;1;2;1];8,[2;1;1;3;1;3];9,[1;2;3;2;3;3];10,[3;1;2;3;2;1];11,[3;1;2;3;2;1];12,[3;1;2;3;2;1]] for y in 0..ps.height - 1 do dic.[y] |> List.iteri (fun x c -> newfield.[x, y] <- enum c) nextPuyo { ps with field = newfield; cheat = true } let detectCollision puyo field = let result = ref false puyo.pattern |> convert |> Array2D.iteri (fun i j c -> c |> function | PuyoColors.n -> () | _ -> puyo.position ||> fun x y -> (x + i,y + j) ||> fun xi yj -> if xi < 0 || xi >= Array2D.length1 field || yj < 0 || yj >= Array2D.length2 field || field.[xi, yj] <> PuyoColors.n then result := true); !result let getAllclearScore ps = if ps.field = Array2D.create ps.width ps.height PuyoColors.n |> not then decimal 0 else decimal 3600 + decimal (getLevel ps.erased * 5) let getScore ps = let rensaBonus n = dict [1,0;2,8;3,16;4,32;5,64;6,96;7,128;8,160;9,192;10,224;11,256;12,288;13,320;14,352;15,388;16,416;17,448;18,480;19,512] |> fun dic -> if n > 19 then dic.[19] else dic.[n] let unitBounus n = dict [4,0;5,2;6,3;7,4;8,5;9,6;10,7;] |> fun dic -> if n > 10 then 10 else dic.[n] let colursBounus n = dict [0,0;1,0;2,3;3,6;4,12;5,24] |> fun dic -> dic.[n] let a,b,c,d = ps.union * 10,rensaBonus ps.rensa,unitBounus ps.union,colursBounus ps.colors // 基本得点,連鎖ボーナス,連結ボーナス,複色ボーナス b + c + d |> fun x -> if x = 0 then a,1 else a,x let fixed' ps = ps.current.pattern |> convert |> fun c -> seq { for i in 0..Array2D.length1 c - 1 do for j in 0..Array2D.length2 c - 1 do c.[i,j] |> function | PuyoColors.n -> () | _ -> ps.current.position ||> fun x y -> ps.field.[x + i, y + j] <- ps.current.pattern.[j].[i] } |> Seq.iter id { ps with current = {ps.current with hidden = true }; falling = true } let fall ps = seq { for x in 0..ps.width-1 do for y in [ps.height-1 .. -1 .. 0] do if ps.field.[x,y] = PuyoColors.n then for z in (y-1) .. -1 .. 0 do if ps.field.[x,z] > PuyoColors.n then ps.field.[x,z+1] <- ps.field.[x,z] ps.field.[x,z] <- PuyoColors.n yield z } |> Seq.length let erase ps = let erase' x y ps = if ps.field.[x,y] = PuyoColors.n then None else let result, list = ref 1, ref [x,y] let rec search x y result = ps.checkField.[x,y] <- true let search' x y retsult f g predicate = if predicate (f x) (g y) ps && ps.checkField.[f x, g y] <> true && ps.field.[f x, g y] = ps.field.[x,y] then result := !result + 1 list := !list@[f x, g y] search (f x) (g y) result search' x y result (fun x -> x+1) id (fun x y ps -> x < ps.width) search' x y result id (fun y -> y+1) (fun x y ps -> y < ps.height) search' x y result (fun x -> x-1) id (fun x y ps -> x > 0) search' x y result id (fun y -> y-1) (fun x y ps -> y > 0) search x y result if !result >= 4 then List.map (fun (x,y) -> x,y,ps.field.[x,y] )!list |> Some else None seq { for x in 0..ps.width-1 do for y in ps.height-1 .. -1 .. 0 do let ecount = erase' x y (clearCheckField ps) |> function |Some x -> x | _ -> [] yield! ecount } |> Seq.distinct |> Seq.toList |> fun etarget -> let colors = List.map (fun (x,y,_) -> ps.field.[x,y]) etarget |> Seq.distinct |> Seq.toList etarget,List.length etarget, List.length colors, { ps with current = { ps.current with pattern = none } } let getUnion ps x y = if x < 0 || x > ps.width - 1 || y > ps.height - 1 || ps.field.[x,y] = PuyoColors.n then Union.None else let rise ps x y = [y..(ps.height-1)] |> List.exists (fun y -> ps.field.[x,y] = PuyoColors.n) let lrunion n b f add = if b || (ps.field.[f x,y] = ps.field.[x,y] && (y = ps.height - 1 || (y < ps.height - 1 && (rise ps x y |> not && rise ps (f x) y |> not)) || (y < ps.height - 1 && (rise ps x y && rise ps (f x) y )))) |> not then n else add n let left n = lrunion n (x = 0) (fun x -> x - 1) ((+) Union.Left) let right n = lrunion n (x >= ps.width - 1) ((+) 1) ((+) Union.Right) let tbunion n f g c = if f || ps.field.[x,g y] = ps.field.[x,y] |> not then n else n + c let top n = tbunion n (y = 0) (fun y -> y - 1) Union.Top let bottom n = tbunion n (y >= ps.height - 1) (fun y -> y + 1) Union.Bottom Union.None |> (top >> left >> right >> bottom)
PuyoGame.fs
namespace FSharpyopuyo open System open System.Runtime.Serialization open Microsoft.Xna.Framework open Microsoft.Xna.Framework.Audio open Microsoft.Xna.Framework.Graphics open Microsoft.Xna.Framework.Input open Microsoft.Xna.Framework.Storage open PuyoPuyoLibrary [<DataContract>] type SaveGameData = { [<field:DataMember(Name="MaxLevel")>] MaxLevel : int; [<field:DataMember(Name="HighScore")>] HighScore : decimal } type PuyoGame () as this = inherit Game() let gametitle, gmanager, sprite = "とことんF#よぷよ!", new GraphicsDeviceManager(this), lazy new SpriteBatch(this.GraphicsDevice) let mutable ps = PuyoPuyo.createState 32 32 6 13 0 let slowTimelag,fastTimelag, fps = 350.,230.,60. let backgroundTexture,puyoTexture,blinkTexture,gameoverTexture,allclearTexture,carbancleTexture,suketoudaraTexture = ["background";"puyopuyo";"blink";"batanQ";"allclear";"carbuncle";"suketoudara"] |> List.map (fun name -> lazy this.Content.Load<Texture2D>(@"Content\image\" + name)) |> function | a::b::c::d::e::f::g::[] -> a,b,c,d,e,f,g | _ -> invalidArg "tlist" "リストの長さが違うよ。" let font = lazy this.Content.Load<SpriteFont>(@"Content\font\SpriteFont1") let gameSe = ["move";"rotate";"drop";"batanQ";"allclear";"pafu";] |> List.map (fun name -> this.Content.Load<SoundEffect>(@"Content\sound\" + name).CreateInstance() |> fun x -> x.Volume <- 0.3f; lazy x) let chainSe = [1..19] |> List.map (fun i -> this.Content.Load<SoundEffect>(@"Content\sound\chain" + if i > 7 then string 7 else string i).CreateInstance() |> fun x -> x.Volume <- 0.3f; lazy x) let chainVoice = [1..19] |> List.map (fun i -> this.Content.Load<SoundEffect>(@"Content\sound\chainvoice" + if i > 5 then string 5 else string i).CreateInstance() |> fun x -> x.Volume <- 0.3f; lazy x) let bgm = (this.Content.Load<SoundEffect>(@"Content\sound\MorningOfPuyoPuyo").CreateInstance() |> fun x -> x.Volume <- 0.2f; x.IsLooped <- true; lazy x) let soundPlay (se:Lazy<SoundEffectInstance>) = se.Force().Play() let checkPauseKey = let keyWait, pauseKeyDown, pauseTimelag = ref 0., ref false, 700. (fun () -> if not ps.gameover then keyWait := !keyWait + fps if ps.pause && !keyWait > pauseTimelag && Keyboard.GetState().IsKeyDown(Keys.F12) then ps <- PuyoPuyo.cheat ps; if !keyWait > pauseTimelag && Keyboard.GetState().IsKeyDown(Keys.P) && !pauseKeyDown then gameSe.[5] |> soundPlay; bgm |> soundPlay; ps <- { ps with pause = false }; pauseKeyDown := false elif !keyWait > pauseTimelag && Keyboard.GetState().IsKeyDown(Keys.P) && not !pauseKeyDown then gameSe.[5] |> soundPlay; bgm.Force().Stop(); ps <- { ps with pause = true }; pauseKeyDown := true if !keyWait > pauseTimelag then keyWait := 0.) let operateKeys = let keyWait = ref 0. (fun () -> keyWait := !keyWait + fps let operateKey key = if not ps.gameover then key |> function | Keys.Z | Keys.Space | Keys.NumPad8 when !keyWait > slowTimelag -> gameSe.[1] |> soundPlay; Some (PuyoPuyo.rotateL ps) | Keys.X | Keys.Up | Keys.NumPad5 when !keyWait > slowTimelag -> gameSe.[1] |> soundPlay; Some (PuyoPuyo.rotateR ps) | Keys.Right | Keys.NumPad6 when !keyWait > slowTimelag -> gameSe.[0] |> soundPlay; Some (PuyoPuyo.move ps PuyoPuyo.Right) | Keys.Left | Keys.NumPad4 when !keyWait > slowTimelag -> gameSe.[0] |> soundPlay; Some (PuyoPuyo.move ps PuyoPuyo.Left) | Keys.Down | Keys.NumPad2 when !keyWait > fastTimelag -> if ps.current.hidden |> not then ps <- { ps with totalScore = (+) ps.totalScore <| decimal 1 } Some (PuyoPuyo.move ps PuyoPuyo.Down) | Keys.Escape -> this.Exit(); None | _ -> None else key |> function | Keys.Enter -> ps <- PuyoPuyo.reset ps; bgm |> soundPlay; None | Keys.Escape -> this.Exit(); None | _ -> None |> function | Some x -> if not (PuyoPuyo.detectCollision x ps.field) then ps <- { ps with current = x } | None -> () let resetWait () = if !keyWait > slowTimelag then keyWait := 0. Keyboard.GetState().GetPressedKeys() |> Array.toList |> List.sort |> List.rev |> function | [x] -> operateKey x; resetWait () | [x;y] -> operateKey x; operateKey y; resetWait () | [x;y;z] | x::y::z::_ -> operateKey x; operateKey y; operateKey z; resetWait () | _ -> () ) let blink = let blinkWait, blink = ref 0., ref false fun () -> if ps.pause then !blink else blinkWait := !blinkWait + fps if !blinkWait > 5. * fps then blink := not !blink; blinkWait := 0. !blink let saveFilename = "puyosys.sav" let saveStorage (device:StorageDevice) (level:int) score = let result = device.BeginOpenContainer(gametitle, null, null) if result.AsyncWaitHandle.WaitOne() then use container = device.EndOpenContainer(result) result.AsyncWaitHandle.Close() if container <> null then if (container.FileExists(saveFilename)) then container.DeleteFile(saveFilename) use stream = container.CreateFile(saveFilename) let serializer = DataContractSerializer(typeof<SaveGameData>) serializer.WriteObject(stream , { MaxLevel = level; HighScore = score }) let loadStorage (device:StorageDevice) = let result = device.BeginOpenContainer(gametitle, null, null) let level,highscore = ref 1, decimal 0 |> ref if result.AsyncWaitHandle.WaitOne() then use container = device.EndOpenContainer(result) result.AsyncWaitHandle.Close() if container <> null && container.FileExists(saveFilename) then use stream = container.OpenFile(saveFilename, System.IO.FileMode.Open ) let serializer = DataContractSerializer(typeof<SaveGameData>) let data = serializer.ReadObject(stream) :?> SaveGameData level := data.MaxLevel; highscore := data.HighScore !level, !highscore let drawPuyo (c:PuyoColors, x, y, ((i, j) as point)) (location:Vector2) rect hw = let fx,fy = float32 (x+i), float32 (y+j) let lx,ly = float32 location.X, float32 location.Y let draw f = sprite.Force().Draw(puyoTexture.Force(), Vector2(32.f * fx + lx,32.f * fy + ly |> f), Nullable rect, Color.White) if point = (1,1) then let texture = if blink () then blinkTexture.Force() else puyoTexture.Force() sprite.Force().Draw(texture, Vector2(32.f * fx + lx,32.f * fy + ly |> f), Nullable rect, Color.White) c |> function | PuyoColors.n -> () | _ -> if hw && ps.current.upside then (fun x -> x - 16.f) |> draw else draw id let uncoupling c adjustx = Rectangle(0, ps.ph * (int c - 1), ps.pw / adjustx, ps.ph) let drawText (msg:string) (v:Vector2) c = sprite.Force() |> function | x -> font.Force() |> fun font -> [font, msg, Vector2(v.X+3.f,v.Y+3.f), Color.Black; font, msg, v, c ] |> List.iter (fun (font, msg, v, c) -> x.DrawString(font, msg, v, c)) let drawLiteral () = ["NEXT",Vector2(240.f, 34.f),Color.MediumSpringGreen; "LEVEL",Vector2(240.f, 322.f),Color.MediumSpringGreen; "SC.",Vector2(32.f, 420.f),Color.HotPink; "HSC.",Vector2(32.f, 2.f),Color.Orange; "MLV.",Vector2(230.f, 2.f),Color.Orange] |> List.iter (fun (s,v,c) -> drawText s v c) let drawScoreAndLevel () = seq { yield ((PuyoPuyo.getLevel ps.erased |> string).PadLeft(3,'0')), Vector2(255.f, 356.f), Color.HotPink if ps.totalScore < ps.highScore then yield string ps.highScore |> fun s -> s, Vector2(224.f - font.Force().MeasureString(s).X, 2.f), Color.Orange else yield string ps.totalScore |> fun s -> s, Vector2(224.f - font.Force().MeasureString(s).X, 2.f) , Color.Orange if ps.etarget = List.empty then yield string ps.totalScore |> fun s -> s, Vector2(224.f - font.Force().MeasureString(s).X, 420.f), Color.HotPink else yield string ps.scoreBase + "×" + (string ps.magnifyingPower).PadLeft(4,' ') |> fun s -> s, Vector2(224.f - font.Force().MeasureString(s).X, 420.f), Color.SkyBlue if (PuyoPuyo.getLevel ps.erased) < ps.maxLevel then yield (string ps.maxLevel).PadLeft(3,'0') , Vector2(280.f , 2.f), Color.Orange else yield (PuyoPuyo.getLevel ps.erased |> string).PadLeft(3,'0') , Vector2(280.f , 2.f), Color.Orange } |> Seq.iter (fun (s,v,c) -> drawText s v c) let drawField = let gameoverWait = ref 0. (fun location () -> let unit ps x y (c:PuyoColors) adjustx = PuyoPuyo.getUnion ps x y |> fun r -> Convert.ToInt32(r |> int |> string) |> fun x -> Rectangle(ps.pw * x,ps.ph * (int c - 1), ps.pw / adjustx, ps.ph) ps.field |> Array2D.iteri (fun i j c -> if j > 0 then drawPuyo(c, i, j, (0, 0)) location (unit ps i j c 1) false) if ps.gameover then sprite.Force().Draw(gameoverTexture.Force(), Vector2(50.f,400.f - float32 !gameoverWait / 40.f), Color(255, 255, 255, 128)) if !gameoverWait <= 12000. then bgm.Force().Stop() gameSe.[3] |> soundPlay gameoverWait := !gameoverWait + fps * 3. else ["つづける:Enter",10.f;"やめる:Esc",50.f] |> List.iter (fun (msg,y) -> drawText msg <| Vector2(45.f, 160.f + y) <| Color.Gold) else gameoverWait := 0. if ps.current.hidden |> not then ps.current.position ||> fun x y -> (ps.current.pattern) |> PuyoPuyoLibrary.PuyoPuyo.convert |> Array2D.iteri (fun i j c -> if y + j > 0 then drawPuyo(c, x , y, (i, j)) location (uncoupling c 1) true) sprite.Force().Draw(backgroundTexture.Force(), Vector2(0.f,0.f),Nullable(Rectangle(0,0,256,32)), Color.White)) <| Vector2(32.f, 0.f) let drawErase = let animeWait = ref 0. (fun f (location:Vector2) () -> if ps.etarget <> List.empty then animeWait := !animeWait + fps let draw union = List.iter (fun (x,y,c) -> if y > 0 then drawPuyo(c, x, y, (0, 0)) location (Rectangle(ps.pw * union ,ps.ph * (int c - 1), ps.pw, ps.ph)) false) ps.etarget if !animeWait < 780. then draw 16 elif !animeWait < 1560. then draw 17 else animeWait := 0.; f()) <| (fun () -> ps <- { ps with etarget = List.empty }) <| Vector2(32.f, 0.f) let drawAnimation (texture:Lazy<Texture2D>) width hight max vector = let animeWait, counter = ref 0., ref 0 (fun (location:Vector2) () -> animeWait := !animeWait + fps if !animeWait > 780. - (30. * float (PuyoPuyo.getLevel ps.erased + 1)) then animeWait := 0.; if not ps.pause then incr counter sprite.Force().Draw(texture.Force(), location, Nullable (Rectangle(width * !counter,0,width,hight)), Color.White) if !counter = max then counter := 0) <| vector let drawCarbancle = drawAnimation <| carbancleTexture <| 25 <| 25 <| 37 <| Vector2(260.f,388.f) let drawSukesoudara = drawAnimation <| suketoudaraTexture <| 72 <| 54 <| 10 <| Vector2(236.f,228.f) let drawNext () = let drawPuyo' pattern location adjustx = pattern |> PuyoPuyoLibrary.PuyoPuyo.convert |> Array2D.iteri (fun i j c -> drawPuyo (c, 1, j, (0, 0)) location (uncoupling c adjustx) false) Vector2(224.f, 96.f) |> fun location -> [ps.next.[0].pattern, location, 1; ps.next.[1].pattern, (Vector2(location.X + 32.f, location.Y + 32.f)), 2] |> List.iter (fun (p,v,a) -> drawPuyo' p v a) let drawAllClear () = if ps.allclear then sprite.Force().Draw(allclearTexture.Force(), Vector2(45.f,50.f), Color.White) let drawPause () = if ps.pause then drawText "PAUSE" <| Vector2(96.f,150.f) <| Color.Gold let drawRensa = let rensaWait, rensaCount = ref 0., ref 0 (fun () -> if ps.pause |> not then rensaWait := !rensaWait + fps * 1.5 if ps.rensa > 0 && !rensaWait < 25. * fps then (3,7) ||> fun x y -> drawText (string ps.rensa + "れんさ") <| Vector2(32.f * float32 x, 32.f * float32 y - float32(!rensaWait / 40.)) <| Color.HotPink if ps.rensa <> !rensaCount && !rensaWait > 50. * fps then rensaCount := ps.rensa; rensaWait := 0. ) let save () = let save maxLevel highScore = let result = StorageDevice.BeginShowSelector(PlayerIndex.One, null, null) let device = StorageDevice.EndShowSelector(result) saveStorage device maxLevel highScore ps <- { ps with maxLevel = maxLevel; highScore = highScore } let maxlevelAndHighScore () = if PuyoPuyo.getLevel ps.erased > ps.maxLevel then PuyoPuyo.getLevel ps.erased else ps.maxLevel , if ps.totalScore > ps.highScore then ps.totalScore else ps.highScore if PuyoPuyo.getLevel ps.erased > ps.maxLevel || ps.totalScore > ps.highScore then maxlevelAndHighScore () ||> fun maxLevel highScore -> save maxLevel highScore let update = let updateWait, updateTimelag = ref 0., ref (72. * fps + (15. * float (PuyoPuyo.getLevel ps.erased))) let chain = (fun cont -> if ps.etarget = List.empty && PuyoPuyo.fall ps > 0 then ps <- { ps with falling = true } updateTimelag := 24. * fps + (15. * float (PuyoPuyo.getLevel ps.erased + 1)) else PuyoPuyo.erase ps |> function | etarget,union,colors,newps when union <= 0 -> ps <- { newps with rensa = 0; union = union; colors = colors } PuyoPuyo.getAllclearScore ps |> fun z -> if z > decimal 0 then gameSe.[4] |> soundPlay ps <- { ps with totalScore = (z |> fun x -> x + ps.totalScore); allclear = true } ps <- { ps with falling = false } cont () if PuyoPuyo.detectCollision ps.current ps.field then ps <- { ps with gameover = true } ; save() updateTimelag := 72. * fps + (15. * float (PuyoPuyo.getLevel ps.erased)) | etarget,union,colors,newps -> List.iter (fun (x,y,_) -> ps.field.[x,y] <- PuyoColors.n) etarget async { newps.rensa |> (fun x -> if x > 19 then chainVoice.[19] else chainVoice.[x]) |> soundPlay } |> Async.Start async { newps.rensa |> (fun x -> if x > 19 then chainSe.[19] else chainSe.[x]) |> soundPlay } |> Async.Start ps <- { newps with rensa = newps.rensa + 1; union = union; colors = colors; erased = newps.erased + union; cheat = false } let scoreBase,magnifyingPower = PuyoPuyo.getScore ps ps <- { ps with totalScore = decimal (scoreBase * magnifyingPower) + ps.totalScore; allclear = false; etarget = etarget; scoreBase = scoreBase; magnifyingPower = magnifyingPower }) fun () -> updateWait := !updateWait + fps + (15. * float (PuyoPuyo.getLevel ps.erased)) if !updateWait > !updateTimelag then updateWait := 0. let puyo = PuyoPuyo.descend ps if PuyoPuyo.detectCollision puyo ps.field |> not then ps <- { ps with current = puyo } if ps.falling && ps.etarget = List.empty then chain (fun () -> ps <- PuyoPuyo.nextPuyo ps) if !updateWait < 36. * fps then ps <- { ps with current = { ps.current with upside = true } } else ps <- { ps with current = { ps.current with upside = false } } do this.Window.Title <- gametitle (320,448) ||> fun x y -> gmanager.PreferredBackBufferWidth <- x ; gmanager.PreferredBackBufferHeight <- y this.TargetElapsedTime <- TimeSpan.FromSeconds(1.0 / fps) override thi.Initialize() = base.Initialize() |> fun _ -> let result = StorageDevice.BeginShowSelector(PlayerIndex.One, null, null) let device = StorageDevice.EndShowSelector(result) loadStorage device ||> fun maxLevel highScore -> ps <- { ps with maxLevel = maxLevel; highScore = highScore } bgm |> soundPlay override this.Update(gameTime) = checkPauseKey () if ps.pause |> not then base.Update gameTime [operateKeys;update] |> List.iter (fun f -> f ()) if not ps.gameover then if PuyoPuyo.detectCollision (PuyoPuyo.descend ps) ps.field then if not ps.falling then gameSe.[2] |> soundPlay; ps <- PuyoPuyo.fixed' ps elif ps.current.pattern = PuyoPuyo.none && not ps.falling then ps <- PuyoPuyo.nextPuyo ps override this.Draw(gameTime) = base.Draw gameTime |> fun _ -> gmanager.GraphicsDevice.Clear(Color.Black) sprite.Force().Begin () sprite.Force().Draw(backgroundTexture.Force(), Vector2.Zero, Color.White) [drawAllClear;drawField;drawNext;drawSukesoudara;drawCarbancle;drawErase;drawRensa;drawLiteral;drawScoreAndLevel;drawPause] |> List.iter (fun f -> f()) sprite.Force().End () override this.EndRun () = base.EndRun(); save() module Program = [<EntryPoint>] let main (args : string[]) = use game = new PuyoGame() in game.Run(); 0
画像データ、音データに関しましては、動画をごらん頂けばわかるとおり配布することはできません。お察しください。
コードを読み理解するとができれば、どんな画像や音データを用意すれば適切に動作させられるかわかるはず。
なお、コードは読めなくてもまったく問題ありません。それでは、CLR/H勉強会でお会いしましょう。