Update 1: added examples about side-scrolling and the reason for a time-scale factor.
Since making Flappy Bird in the Wolfram Language, I decided to up the ante with a playable demo of a video game classic, Castlevania for the Nintendo Entertainment System. To limit the scope of the project, I only recreated the first stage, but modified it to play like "horde mode" where enemies continuously spawn:
The package can be downloaded from my github account: CastlevaniaDemo_WolframLanguage.
Controls
I wanted to avoid using keyboard events since they block, i.e. you can't hold the right-arrow key and press another key (to jump) at the same time. Instead, I use an Xbox controller because I'm on a Windows computer and it has the device drivers already installed. It makes connecting the controller as easy as turning in on. If you want to use a different controller, then you may need to re-map some of the ControllerState
s.
I tried to mimic the original controls where you can move left and right, as well as crouch or stand using the directional pad. Only two buttons are used: one to jump and one to crack the whip. The crouch doesn't help much in such a simple level, but it's there nonetheless.
Design
Side-scrolling
A 2-D side-scroller moves the screen as the player advances. This is straightforward to achieve using a dynamic PlotRange
. I found the image from a basic Google search, cropped it, and used Inset
to place it in a Graphics
expression. To put everything in an understandable coordinate system, I adjusted the relative size (4.19) until the image spanned the vertical space from 0 to 1. The single static image background
is
Side-scrolling is implemented by updating the PlotRange
dynamically over an otherwise static graphic:
Manipulate[
Graphics[
Inset[background, {0, 0}, Scaled[{0, 0}], 4.19],
Frame -> True, PlotRangeClipping -> True,
PlotRange -> {{Dynamic[scrollPos], Dynamic[screenWidth + scrollPos]}, {0, 1}}
],
{{scrollPos, 0, "Screen Position"}, 0, 3},
{{screenWidth, 1, "Screen Width"}, 1, 2}
]
The above code is a demonstration, whereas the game updates scrollPos
as the player moves. There are additional restrictions based on where the player is located, such that the player can walk to the edge of the screen, but the screen stops moving when the player is close.
In the following, csX
is the x-direction on the directional pad. Its value is +1 if held to the right and -1 if held to the left.
Attributes[moveRight] = {HoldRest};
moveRight[csX_, fps_] := (
(* face player in direction of movement *)
If[!playerFacingRight, playerFacingRight=True];
(* update player sprite and screen position *)
With[{p=playerX+(playerXvel/fps)*csX}, If[p < 3.55, playerX=p]];
If[scrollPos < 2.7 && ((scrollPos+screenWidth)/2.4 < playerX), scrollPos+=(scrollSpeed/fps)]
);
Attributes[moveLeft] = {HoldRest};
moveLeft[csX_, fps_] := (
(* face player in direction of movement *)
If[playerFacingRight, playerFacingRight=False];
(* update player sprite and screen position *)
With[{p=playerX+(playerXvel/fps)*csX}, If[0.07 < p, playerX=p]];
If[scrollPos > 0 && (scrollPos+screenWidth/2.3 > playerX), scrollPos-=(scrollSpeed/fps)]
);
Controlling Update Speed
Instead of using a ScheduledTask
to update at a fixed frame rate, I opted to let Dynamic
update as fast as possible. This is reminiscent of old computer games that were tied to the processor's frequency. To get around this I use a global variable fps
to slow down the apparent motion. Said differently, the front end updates as fast as possible, but the distance a sprite moves per kernel-state-update is proportional to fps
.
The previous section shows fps
in the player update:
p = playerX + (playerXvel / fps)*csX
The player's horizontal-position playerX
is updated by the fixed horizontal-velocity playerXvel
, but proportional to fps
. If fps
is larger, then the distance traveled per-kernel-update is smaller.
For every moving sprite, however, the front end has to communicate more with the kernel. This causes the entire game to slow down proportionally to the number of dynamically updating expressions. I more-or-less get around this detail by linearly adjusting the global fps
variable proportional to the number of active sprites. The rough idea is that if each sprite takes 2 units of fps
time to update, then I should take away 2 units for each active enemy.
As a toy example of this behavior, look at the following independent example:
fps = 10;
objList = <||>;
createShape[index_Integer] := Module[{pos = RandomReal[{-30, 0}, 2]},
object[index] := (
(* updated object position *)
pos += 0.3/fps;
(* if object leaves the screen, remove it from the list of objects, otherwise output graphic *)
If[pos.pos > 30^2,
objList = KeyDrop[objList, index]; object[index] =.,
{EdgeForm[Directive[Thick]], FaceForm[None], Rectangle[pos, pos + {5, 5}]}
]
);
objList = <|objList, index :> object[index]|>
]
i = 1;
Column[{
Button["Add", createShape[i++]],
Graphics[Dynamic[Values[objList]],
PlotRange -> {{-30, 30}, {-30, 30}}, Frame -> True,
ImageSize -> Medium],
Dynamic[Length[objList]]
}]
Ignoring that this code produces a front-end memory leak, what you should observe is that the rectangles motion slows down as more are added. Once a few rectangles leave the "world", the remaining rectangles speed up.
In my game code where I create enemies, I use a variable tsf
(time-scale-factor) which is just a local renaming of fps
. It decreases when an enemy is put in motion i.e. initialized, and increases when the enemy dies or leaves the screen. Below I only show the parts where tsf
is adjusted. speedTrigger
is used as a flag to prevent tsf
from updating more than once.
initializeEnemy[index, "ghoulRight"] := (
. . .
speedTrigger=True; tsf-=2;
. . .
enemy[index] := (
If[ !dying
(* if exited map *)
If[xPos > 3.8,
. . .
If[speedTrigger, tsf+=2; speedTrigger = False];
. . .
],
(* if dead, after death animation, position enemy off screen, update score *)
If[speedTrigger, tsf+=2; speedTrigger = False];
. . .
]
)
)
Sprites before hit boxes
It makes more sense to place and size the sprites into the graphics before I create hit boxes. Sprite maps can be found online for many classic games. I found a character map for the main character sprite and cropped each frame. There are 12 unique frames in total for my chosen character movement, but for smooth animations some frames can be found in multiple sequences:
- four frames for whipping while standing
- four frames for whipping while crouched
- three frames for character death
The above images have already been adjusted. First, I applied RemoveBackground
and ImageCrop
to each frame. If you naively stopped here and simply updated the first argument of Inset
with these images, then your character would appear glitchy when switching between images. The reason is that the inset images are of different sizes. I needed to "normalize" the images to a common size.
The whip frames have the largest extent (see the above images that are selected). The whip hangs below the feet while crouched. It also extends far to the left/right when fully extended. I chose common image size of 105 x 42. I first used ImagePad
to pad each image up to the chosen size. Then I created functions for fine-tuning the sprite positions:
imageRotateLeft[im_, amount_] := If[amount == 0,
im,
ImageAssemble[{{ImageTake[im, {1, -1}, {1 + amount, -1}], ImageTake[im, {1, -1}, {1, amount}]}}]
]
imageRotateRight[im_, amount_] := If[amount == 0,
im,
ImageAssemble[{{ImageTake[im, {1, -1}, {-amount, -1}], ImageTake[im, {1, -1}, {1, -1 - amount}]}}]
]
I use a combination of Manipulate
expressions to align and center each frame. One aligns one frame with another:
Manipulate[
left = tt[[1]]; right = tt[[2]];
Framed[ImageCompose[left, imageRotateLeft[right, n]], FrameMargins -> None],
{n, 0, 10, 1}
]
The other aligns a frame with its reflection:
Manipulate[
left = tt[[m]]; right = ss[[m]];
Framed[ImageCompose[imageRotateRight[left, n], imageRotateLeft[right, n]], FrameMargins -> None],
{n, 0, 10, 1}, {m, 1, 4, 1}
]
Having a reflection-aligned set of frames is useful when making hit boxes. That way we only need one hit box for standing and one for crouching, regardless of facing right or left. Moreover, having "one-size-fits-all" for the images, the inset size argument remains fixed.
The ghoul enemy proceeds similarly, but is simpler since it is only two animation frames and does not include a moving weapon.
Character Controls
I use a Module
to mimic "static" variables for the player character.
Module[{playerFacingRight=True, playerCrouched=False, playerX=0.288, playerY=0.282,
playerXvel=0.2, scrollSpeed=0.2, im=walkingRight[[1]],
playerAnimationCounter=1, playerFrame=1, walkAnimationDelay=25,
grav=0.5, jumpVel=0.5, playerYvel=0, jumpCounter=0, previousJumpState=False,
whipFrame=1, whipAnimationCounter=1, previousWhipState=False, whipping=False, whipAnimationDelay=8,
whipStandLeftBox,whipStandRightBox,whipCrouchedLeftBox,whipCrouchedRightBox,
crouchedBox,standingBox,playerBox,whipSoundCheck=True},
Attributes[standWhipUpdate] = {HoldAll};
standWhipUpdate[whipBox_] := ( . . . )
Attributes[crouchWhipUpdate] = {HoldAll};
crouchWhipUpdate[whipBox_] := ( . . . )
Attributes[moveRight] = {HoldRest};
moveRight[csX_, fps_] := ( . . . )
Attributes[moveLeft] = {HoldRest};
moveLeft[csX_, fps_] := ( . . . )
Attributes[character] = {HoldRest};
character[{csX_,csY_,csB_, csB2_}, enemyBox_, whipBox_, fps_, playerDead_] := (
.
.
.
Inset[im, {playerX,playerY}, Scaled[{0.5,0.5}], 0.562]
)
]
Within this module I define a number of functions that share the scoped module variables. The main function is character
that, after processing the current state of the player, returns the Inset
graphics primitive.
The cs*
inputs are ControllerState["X3"]
, ControllerState["Y3"]
, ControllerState["B1"]
, and ControllerState["B2"]
, respectively. The other inputs are the hit boxes, time delay, and a predicate flag for whether the player is dead.
The character
function logic is as follows:
- Has the character hit an enemy?
- yes -> you're dead
- no -> you're not dead
- If not dead, then
- Check the jump button
- Check the whip button
- Add gravity effect (you fall regardless of whether you're alive)
- if not dead,
- if jumping,
- else you're walking
- are you whipping?
- yes
- no -> update normal walking controls
- else you're dead -> update death animation
- output Inset primitive with updated position and sprite image
For example, while on the ground and not whipping, I use a Which
expression to decide the update:
Which[
(* walking to the right *)
csX==1 && (csY==0 || csY==1),
moveRight[csX, fps];
playerCrouched=False;
(* animate character sprite *)
playerAnimationCounter += 1;
If[Mod[playerAnimationCounter,walkAnimationDelay]==0,
playerAnimationCounter=1;
im=walkingRight[[If[playerFrame==4,playerFrame=1,++playerFrame]]]
],
(* walking to the left *)
csX==-1 && (csY==0 || csY==1),
moveLeft[csX, fps];
playerCrouched=False;
(* animate character sprite *)
playerAnimationCounter += 1;
If[Mod[playerAnimationCounter,walkAnimationDelay]==0,
playerAnimationCounter=1;
im=walkingLeft[[If[playerFrame==4,playerFrame=1,++playerFrame]]]
],
(* crouched to the right *)
csX==1 && csY==-1,
If[!playerFacingRight, playerFacingRight=True];
playerCrouched=True;
playerAnimationCounter=9;
im=crouchRight,
(* crouched to the left *)
csX==-1 && csY==-1,
If[playerFacingRight, playerFacingRight=False];
playerCrouched=True;
playerAnimationCounter=9;
im=crouchLeft,
(* crouch down *)
csX==0 && csY==-1,
If[!playerCrouched,
playerCrouched=True;
im = If[playerFacingRight, crouchRight, crouchLeft]
],
(* stand up *)
csX==0 && csY==1,
playerFrame=1;
If[playerCrouched,
playerCrouched=False;
im = If[playerFacingRight, walkingRight, walkingLeft][[playerFrame]]
]
]
You'll see that the animation of the character involves a delayed update. So not only am I slowing the apparent motion with the fps
variable, I also slow the apparent animation by using a playerAnimationCounter
with a fixed walkAnimationDelay
.
I did include a double jump, but it is also somewhat irrelevant for the simplicity of the level.
(* check state of jump button *)
Switch[{previousJumpState,csB},
{False,True}, If[jumpCounter<2,jumpCounter+=1;playerYvel=(jumpVel/fps);previousJumpState=True],
{True,False}, previousJumpState=False;
];
Enemy Controls
I use a Module
to mimic "static" variables for the enemies as well.
Attributes[createEnemy] = {HoldRest};
createEnemy[index_Integer, whipBox_, enemyBox_, tsf_, score_, enemyReady_] := Module[
{xPos, yPos, xVel, yVel, im, speedTrigger=False, dying=False, size,
enemyAnimationCounter=1, enemyFrame=1, enemyAnimationDelay=25},
enemyBox = {{0,0},{0,0}};
initializeEnemy[index, "ghoulRight"] := ( . . . )
initializeEnemy[index, "ghoulLeft"] := ( . . . )
]
Within this module I define a number of functions that share the scoped module variables. The main function is initializeEnemy
that itself defines another function enemy
. It seems complicated, but this type of scoping helps avoid memory leaks and treats each enemy as an independent object. The enemy
function processes the current state of the enemy and returns an Inset
graphics primitive.
Adding a new type of enemy is straightforward; create a new initializeEnemy
code block with an overloaded patter e.g. initializeEnemy[index, "medusaRight"]
.
The ghoul logic is much simpler:
- is the enemy dead?
- no
- update position
- have you hit the whip box? yes -> set dead flag
- have you left the screen? yes -> move off screen and set ready flag
- yes -> do death animation and reset
- output Inset primitive with updated position and sprite image
For example, the movement logic is much simpler than the character. It only moves left or right.
initializeEnemy[index, "ghoulLeft"] := (
. . .
enemy[index] := (
If[!dying,
(* if alive *)
(* update position and enemy hit box *)
xPos += xVel/tsf;
enemyBox = {{-0.037,-0.075}+{xPos,yPos},{0.029,0.081}+{xPos,yPos}};
. . .
]
)
. . .
)
Hit Box Management
The hit boxes follow the Rectangle
syntax in that they specify a box by the lower-left and upper-right corners only. All hit boxes are un-rotated rectangles. I check whether the hit boxes are left/right or above/below of each other. If they are not, then they must be overlapping.
impactQ = Compile[{{box1,_Real,2},{box2,_Real,2}},
(* if one hitbox is on the left side of the other *)
If[box1[[2,1]] < box2[[1,1]] || box1[[1,1]] > box2[[2,1]], Return[False]];
(* if one hitbox is above the other *)
If[box1[[2,2]] < box2[[1,2]] || box1[[1,2]] > box2[[2,2]], Return[False]];
True
];
The enemies and player's hit boxes are given as relative distances from their centers. For example,
(* define character hit boxes *)
whipStandLeftBox = {{-0.2`,0.02`},{-0.06`,0.07`}};
whipStandRightBox = {{0.06`,0.02`},{0.2`,0.07`}};
whipCrouchedLeftBox = {{-0.204`,-0.02`},{-0.052`,0.034`}};
whipCrouchedRightBox = {{0.052`,-0.02`},{0.204`,0.034`}};
crouchedBox = {{-0.02`,-0.06`},{0.02`,0.038`}};
standingBox = {{-0.02`,-0.06`},{0.02`,0.086`}};
playerBox = standingBox + {{playerX,playerY},{playerX,playerY}};
I determined the hit boxes using a Manipulate:
Manipulate[
playerX = 0.288; playerY = 0.282; im = ImageReflect[Import["whip3.png"], Right];
Graphics[
{Inset[im, {playerX, playerY}, Scaled[{0.5, 0.5}], 0.562],
FaceForm[None], EdgeForm[Directive[Red, Thick]],
Dynamic[ Rectangle[{playerX + left, playerY + bottom}, {playerX + right, playerY + top}] ]
},
Frame -> False, ImageSize -> Medium, PlotRange -> {{0, 1}, {0, 0.5}}
]
,
{{left, -0.2}, -0.5, 0.5, LabeledSlider},
{{bottom, -0.2}, -0.5, 0.5, LabeledSlider},
{{right, 0.2}, -0.5, 0.5, LabeledSlider},
{{top, 0.2}, -0.5, 0.5, LabeledSlider}
]
The whip is more complicated. The whip's hit box is only active when the whip is fully extended, and its position depends on which direction the player is facing. For example, while in the air movement is allowed. Note in the following that whipframe
#3 is when the whip box is active. Otherwise, within the standWhipUpdate
function the whip box is moved off screen so it doesn't hit anything.
(* while in the air *)
playerFrame=1; playerCrouched=False;
Which[
csX==1, moveRight[csX, fps],
csX==-1, moveLeft[csX, fps]
];
If[whipping,
(* if whipping, player is in standing position *)
standWhipUpdate[whipBox];
(* whipping in the air is unique as movement is allowed; hit boxes need to update with the player's movement *)
playerBox = standingBox;
If[whipFrame==3, whipBox = If[playerFacingRight, whipStandRightBox, whipStandLeftBox] + {{playerX,playerY},{playerX,playerY}}],
(* if not whipping, the legs tuck midway through the jump *)
If[-(jumpVel/fps)/1.5 < playerYvel < (jumpVel/fps)/1.5,
playerBox = crouchedBox;
im = If[playerFacingRight, crouchRight, crouchLeft],
playerBox = standingBox;
im = If[playerFacingRight, walkingRight, walkingLeft][[playerFrame]];
];
]
The hit boxes are passed around between the functions. The player only cares about the enemy hit boxes:
If[
AnyTrue[{enemyBox[1],enemyBox[2],enemyBox[3],enemyBox[4],enemyBox[5],
enemyBox[6],enemyBox[7],enemyBox[8],enemyBox[9],enemyBox[10]},
impactQ[playerBox,#]&
],
playerDead = True; playerCrouched=True;
playerBox = {{1,1},{1,1}};
playerFrame=1;
im = If[playerFacingRight, playerDeathRight, playerDeathLeft][[playerFrame]];
playerAnimationCounter=1;
];
While the enemies only care about the whip hit box:
If[impactQ[whipBox,enemyBox],
dying = True; EmitSound[hitSound]; score++;
enemyBox = {{0,0},{0,0}};
size = 0.035;
enemyFrame=1;
im = enemyDeath[[enemyFrame]];
enemyAnimationCounter=1;
];
Final bells and whistles
The final steps include putting all the code into a larger DynamicModule
. I added a static pause screen that also displays the controls as a PaneSelector
. You can pause the game theoretically at any time, however the other dynamic updates can sometimes block the button. It works better by pressing and holding it until the screen appears, or by holding a direction on the directional pad and then press "start".
The "restart" button, that is only active after you die, has the same issue. I haven't figured out how to fix this.
When you restart after dying, you start in a crouched whipping position and your score is reset.
I also added sound effects for the whip and when you hit an enemy. I added the option for background music as an available MIDI file from the (Video Game Music Headquarters), since the song "Vampire Killer" is so iconic, but I did not loop it.
The enemies are generated based on a RandomVariate
taken from a NormalDistribution
with mean 100 'cycles' and a wide variance.
playLevel[] := Deploy@DynamicModule[{whipBox={{-10,-10},{-5,-5}}, fps=50,
enemyBox, playerDead=False, score=0, enemyReady, previousButtonState=False, pressed=False, enemyCounter=1,
enemyVariate=RandomVariate[NormalDistribution[100,30]]},
(* start background music *)
(*EmitSound[music];*)
(* use PaneSelector to toggle between pause screen and active game *)
PaneSelector[
{
True ->
Column[{
Graphics[
{
Inset[background, {0,0}, Scaled[{0,0}], 4.19],
Dynamic[Inset[Style[score,Red,Bold,27], {scrollPos+screenWidth-0.1,0.9}]],
Dynamic[
If[playerDead,
If[ControllerState["B7"],
playerDead=False; score=0; character[{1,-1,False,True}, enemyBox, whipBox, fps, playerDead]
];
Inset[Panel[Style[" Play again? \nPress Restart Button.",Red,Bold,27],Background->Black], {scrollPos+screenWidth*0.5,0.5}],
{}
]
],
Dynamic[{
If[(enemyCounter += 1) > enemyVariate,
enemyCounter=1; enemyVariate=RandomVariate[NormalDistribution[100,30]];
initializeEnemy[
First[Pick[{1,2,3,4,5,6,7,8,9,10}, enemyReady /@ {1,2,3,4,5,6,7,8,9,10}], {}],
RandomChoice[{"ghoulLeft","ghoulRight"}]
]
];
enemy[1],enemy[2],enemy[3],enemy[4],enemy[5],
enemy[6],enemy[7],enemy[8],enemy[9],enemy[10],
character[
{
ControllerState["X3"],ControllerState["Y3"],
ControllerState["B1"],ControllerState["B3"]
},
enemyBox, whipBox, fps, playerDead
]
}]
},
Frame -> False,
ImageSize -> Large,
PlotRange -> {{Dynamic[0+scrollPos],Dynamic[screenWidth+scrollPos, TrackedSymbols:>{scrollPos}]}, {0,1}},
PlotRangePadding -> None,
PlotRangeClipping -> True,
ImagePadding -> 1
](*,
LabeledSlider[Dynamic[fps],{1,72}]*)
}],
False -> controls
},
Dynamic[
Switch[{previousButtonState,ControllerState["B8"]},
{False,True}, If[pressed,pressed=False,pressed=True]; previousButtonState=True,
{True,False}, previousButtonState=False
];
pressed
],
ImageSize -> Automatic
](* end PaneSelector *),
Initialization :> (
Do[With[{i=i}, createEnemy[i, whipBox, enemyBox[i], fps, score, enemyReady]], {i,1,10}];
enemy[1]=enemy[2]=enemy[3]=enemy[4]=enemy[5]=enemy[6]=enemy[7]=enemy[8]=enemy[9]=enemy[10] = {};
enemyReady[1]=enemyReady[2]=enemyReady[3]=enemyReady[4]=enemyReady[5] = True;
enemyReady[6]=enemyReady[7]=enemyReady[8]=enemyReady[9]=enemyReady[10] = True;
)
]