Message Boards Message Boards

Castlevania Stage 1 Demo

Posted 7 years ago

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:

Gameplay

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 ControllerStates.

enter image description here

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

Background

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}
]

Scroll Position

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]]
  }]

enter image description here

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 walking

enter image description here

  • four frames for whipping while standing

enter image description here

  • four frames for whipping while crouched

enter image description here

  • three frames for character death

enter image description here

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}
]

enter image description here

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}
]

Reflection Alignment

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,
      • are you whipping?
        • yes
        • no
    • 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}
]

enter image description here

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;
)   
]
POSTED BY: Kevin Daily
4 Replies
Posted 7 years ago

This is awesome!

POSTED BY: Parth Pratap

enter image description here - Congratulations! This post is now a Staff Pick as distinguished on your profile! Thank you, keep it coming!

POSTED BY: Moderation Team

Yes, it really is.

POSTED BY: John Fultz

Nice work!

POSTED BY: Ian Hojnicki
Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard

Group Abstract Group Abstract