다음을 통해 공유


Visual Basic: Knife Thrower


Introduction

Hang on to your hats boys and girls! Knife Thrower is sort of a 2D knife throwing game, only there are no levels, points, or moving up in the game...

Just endless (and pointless!) GDI+ knife throwing bliss...

Please note: In order to fully experience this example, you would need to download the complete example project, located at the link at the bottom of this page. While you're there, be sure to take the time to vote 5 stars!

This example/article will cover some topics in the following areas (but may not cover every constituent of that topic!)

  1. GDI+
  2. Transformation of polygons.

Glossary/MSDN Library Cross Reference of terms

This section contains shortcuts to key MSDN library entries of some topics mentioned in this article.

Graphics Class 

What is GDI+?

GDI+ stands for "Graphics Device Interface". Simply put, GDI+ is an easy way to create graphics in Visual Basic and other languages. GDI+ could be used to modify anything from which you can retrieve a "Graphics" object. This example will be using the Graphics Object located in the "Protected Overrides Sub OnPaint". The reason we will use that one instead of the Paint event handler is because this will allow our painting to be processed before the "Control.OnPaint" method is invoked. This will ensure our painting is finished before all the other resultant events in the event chain are raised thus handling our graphics routine first.

Let me back up... Maybe I went too far ahead....

A little more information on GDI+, rather than what we will be doing. GDI+ classes, methods, and objects can be accessed from the following namespaces:

System.Drawing

System.Drawing.Configuration

System.Drawing.Design

System.Drawing.Drawing2D

System.Drawing.Imaging

System.Drawing.Printing

System.Drawing.Text

The graphics object is located in the System.Drawing namespace. Once a graphics object is obtained from an (Image, Bitmap, or Control), there are many drawing methods at your disposal, awaiting your command on how to modify the appearance of that (Image, Bitmap, or Control). There are so many methods in the Graphics Class alone! The graphics class is absolutely essential for doing anything with GDI+. I suggest familiarizing yourself with those methods, described at the following link:

MSDN Library - Graphics Class

​One last thing I want to point out is that you will also need to be somewhat familiar with the contents of the System.Drawing namespace. You can review that at the following link:

MSDN Library - System.Drawing Namespace This explanation of GDI+ has been no where near extensive, as covering it in a single article would simply be paraphrasing the MSDN library. Instead this example is a little more about ingenuity.

What is a polygon in Visual Basic?

A polygon is a shape made up of connected lines. In Visual Basic, a polygon is an array of points, those points are later connected using GDI.

I would like to clarify that my example will utilize the following object that I have created. The name of this object is Polygon, but this is only because I use this object to transform the rotation of the actual polygon points, so in a sense, it is a polygon, but still the polygon is the actual points connected with GDI.

Here is the polygon class I have created for this example (Don't worry, I will explain it later....).

001.Option Strict On
002.Public Class  Polygon
003. Public Points As Point()
004. Public Origin As Point
005. Public Polygon As Point()
006. Private sPoints As Point()
007. Public tsPoints As Point()
008. Sub New(Points As  Point(), Origin As  Point, SpecialPoints As  Point())
009.  Me.Points = Points
010.  Me.Origin = Origin
011.  Me.sPoints = SpecialPoints
012.  Angle = 0
013. End Sub
014. Private _Angle As Single
015. Public Sub UpdateOrigin(NewOrigin As  Point)
016.  Dim Diff As New Point
017.  Diff.X = NewOrigin.X - Origin.X
018.  Diff.Y = NewOrigin.Y - Origin.Y
019.  Me.Origin = NewOrigin
020.  AdjustPoints(Diff.X, Diff.Y)
021. End Sub
022. Sub AdjustPoints(Xadj As Integer, Yadj As  Integer)
023.  For I As Integer  = 0 To  Me.Points.Count - 1
024.   Me.Points(I).X += Xadj
025.   Me.Points(I).Y += Yadj
026.  Next
027.  For I As Integer  = 0 To  Me.Polygon.Count - 1
028.   Me.Polygon(I).X += Xadj
029.   Me.Polygon(I).Y += Yadj
030.  Next
031.  For I As Integer  = 0 To  Me.sPoints.Count - 1
032.   Me.sPoints(I).X += Xadj
033.   Me.sPoints(I).Y += Yadj
034.  Next
035. End Sub
036. Public ReadOnly  Property ContainerRectangle(Pad As Integer) As  Rectangle
037.  Get
038.   Dim Left As Integer  = Me.Polygon(0).X
039.   Dim Right As Integer  = Me.Polygon(0).X
040.   Dim Top As Integer  = Me.Polygon(0).Y
041.   Dim Bottom As Integer  = Me.Polygon(0).Y
042.   For Each  P As  Point In  Me.Polygon
043.    If P.X < Left Then Left = P.X
044.    If P.X > Right Then Right = P.X
045.    If P.Y < Top Then Top = P.Y
046.    If P.Y > Bottom Then Bottom = P.Y
047.   Next
048.   Left -= Pad
049.   Right += Pad
050.   Top -= Pad
051.   Bottom += Pad
052.   Dim R As New Rectangle
053.   R.Location = New  Point(Left, Top)
054.   R.Width = Right - Left
055.   R.Height = Bottom - Top
056.   Return R
057.  End Get
058. End Property
059. Public Property  Angle As  Single
060.  Get
061.   Return _Angle
062.  End Get
063.  Set(value As  Single)
064.   _Angle = value
065.   If _Angle = 360 Then _Angle = 0
066.   If _Angle = -360 Then _Angle = 0
067.   If _Angle > 360 Then
068.    Do
069.     _Angle -= 360
070.    Loop Until  _Angle <= 360
071.   End If
072.   If _Angle < -360 Then
073.    Do
074.     _Angle += 360
075.    Loop Until  _Angle >= 0
076.   End If
077.   If _Angle = 360 Then _Angle = 0
078.   If _Angle = -360 Then _Angle = 0
079.   If _Angle < 0 Then _Angle += 360
080.   RotateToAngle(_Angle)
081.  End Set
082. End Property
083. Private Sub RotateToAngle(Angle As  Single)
084.  Dim Points As New List(Of Point)
085.  For Each  P As  Point In  Me.Points
086.   Points.Add(RotatePoint(P, Me.Origin, Angle))
087.  Next
088.  Dim sppts As New List(Of Point)
089.  For Each  p As  Point In  Me.sPoints
090.   sppts.Add(RotatePoint(p, Me.Origin, Angle))
091.  Next
092.  Me.Polygon = Points.ToArray
093.  Me.tsPoints = sppts.ToArray
094. End Sub
095. Private Function  RotatePoint(ByRef  pPoint As  Point, ByRef  pOrigin As  Point, ByVal  Degrees As  Single) As Point
096.  Dim Result As New Point
097.  Result.X = CInt(pOrigin.X + (Math.Cos(Rad(Degrees)) * (pPoint.X - pOrigin.X) - Math.Sin(Rad(Degrees)) * (pPoint.Y - pOrigin.Y)))
098.  Result.Y = CInt(pOrigin.Y + (Math.Sin(Rad(Degrees)) * (pPoint.X - pOrigin.X) + Math.Cos(Rad(Degrees)) * (pPoint.Y - pOrigin.Y)))
099.  Return Result
100. End Function
101. Private Function  Rad(ByVal  Degree As  Single) As Single
102.  Return Degree / 180 * CSng(Math.PI)
103. End Function
104. Public Function  Clone() As  Polygon
105.  Dim TestPoly As New Polygon
106.  TestPoly.SetAll(Me.Points, Me.Origin, Me.Polygon, Me.sPoints, Me.tsPoints, Me.Angle)
107.  Return TestPoly
108. End Function
109. Private Sub SetAll(Points As  Point(), Origin As  Point, Polygon As  Point(), sPoints As  Point(), tsPoints As  Point(), Angle As  Single)
110.  ReDim Me.Points(Points.Count - 1)
111.  Points.CopyTo(Me.Points, 0)
112.  Me.Origin = New  Point(Origin.X, Origin.Y)
113.  ReDim Me.Polygon(Polygon.Count - 1)
114.  Polygon.CopyTo(Me.Polygon, 0)
115.  ReDim Me.sPoints(sPoints.Count - 1)
116.  sPoints.CopyTo(Me.sPoints, 0)
117.  ReDim Me.tsPoints(tsPoints.Count - 1)
118.  tsPoints.CopyTo(Me.sPoints, 0)
119.  Me._Angle = Angle
120. End Sub
121. Public ReadOnly  Property KnifeDirection As Direction
122.  Get
123.   Select Case  True
124.    Case Me.ContainerRectangle(0).Width > Me.ContainerRectangle(0).Height ' Knife is horizontal
125.     Select Case  True
126.      Case Me.Polygon(0).X > Me.Polygon(11).X
127.       Return Direction.Left
128.      Case Else
129.       Return Direction.Right
130.     End Select
131.    Case Me.ContainerRectangle(0).Width < Me.ContainerRectangle(0).Height 'Knife is vertical
132.     Return Direction.Vertical
133.    Case Else
134.     Return Direction.PerfectDiagnol
135.   End Select
136.  End Get
137. End Property
138. Public Enum  Direction
139.  Up
140.  Down
141.  Left
142.  Right
143.  Unknown
144.  Horizontal
145.  Vertical
146.  PerfectDiagnol
147. End Enum
148. Sub New()
149. End Sub
150.End Class

How does polygon rotation work?

When you want to rotate a polygon, you're really just rotating a set of points around an origin. Therefore you could use a simple one dimensional array of points. You then use the rotation algorithm to rotate each point in that array. The way this works is to take the following equation (Visual Basic Function):

1.Private Function  RotatePoint(ByRef  pPoint As  Point, ByRef  pOrigin As  Point, ByVal  Degrees As  Single) As Point
2. Dim Result As New Point
3. Result.X = CInt(pOrigin.X + (Math.Cos(Rad(Degrees)) * (pPoint.X - pOrigin.X) - Math.Sin(Rad(Degrees)) * (pPoint.Y - pOrigin.Y)))
4. Result.Y = CInt(pOrigin.Y + (Math.Sin(Rad(Degrees)) * (pPoint.X - pOrigin.X) + Math.Cos(Rad(Degrees)) * (pPoint.Y - pOrigin.Y)))
5. Return Result
6.End Function

pPoint is the point that you want to rotate. pOrigin is the origin that location that which your point is rotating around. Degrees is the total distance in degrees, around the origin, that you would like to move the point. If you still do not understand point rotation, try watching the following youtube video to see if this helps you... If not, just know this function rotates points....

Youtube Video - Trigonometry 1

Part1 Form Initialization

This is where we will begin. When I create an application, I always try to keep 'Option Strict On'. This is to help prevent type cast errors. 

In my Form1 (the main form) I have created some class level variables for settings and whatnot. I will explain what each of those variables is for:

  • Knife - This is an instance of the polygon class listed above, this object will be used to calculate a few different things, which I will go into detail later.
  • Origin - This point will be in the center of the knife(The knife's heavy spot), the entire knife object will rotate around this point. Also, if this point moves, then the entire knife object moves as well.
  • Velocity - This Integer represents the increment(pixel count) at which the origin will be translated during each frame.
  • SpinSpeed - This single precision number represents how many degrees the knife will spin per frame.
  • iScale - This single precision number represents the multiplier that will scale the knife from its original size.  Recommended value is (0.2-0.5)
  • WireFrameOnly - This Boolean value determines whether complete graphics will be rendered, or if only a wire frame will be rendered, this can be switched on and off at runtime by pressing the 'z' key.
  • LimitX - This integer represents the rightmost limit of where the user can position the knife before throwing it.
  • TargetLimitX - This integer represents the leftmost bound that the random target generator can generate a target. The random target generator will not generate a rectangle that has a .Left property that is lower that TargetLimitX.
  • Thrown - This variable basically toggles the functionality of the input key 'SpaceBar'. Basically, if the user has thrown a knife, and it sticks, the knife will remain stuck into the target until the user presses spacebar again, at which time this variable will be toggled again. Note* If the knife does not stick, meaning if the point of the knife does not enter the target first. Then the knife position will automatically be reset, and hence this variable will automatically be reset too. The only time the user resets this variable is if the target has been hit.
  • Throwing - This variable indicates that the thread is currently processing the throwing routine. This variable is used to prevent multiple concurrent instances of the throwing routine. The only reason it is possible to create multiple concurrent instances of the throwing routine on a single thread is because the throwing routine contains a loop that contains a call to "Application.DoEvents", which means that each loop would hypothetically take the time to yield processor resources to each other, effectively 'taking turns using the processor'... What does this mean to us? This is NOT GOOD! We are trying simulate the motion of a knife here! We don't need two routines fighting with each other to put the knife in different places that are out of sync. So basically, this variable is checked before starting the throwknife routine, set true before throwing a knife, and set false after the knife has been thrown.
  • OriginalOrigin - This Point variable is set when the knife is created, and will not be modified again! This basically gives us a location within the 'throwing area' to put the knife back in after the knife has been thrown.
  • R - This is a random number generator used in a function called 'RandomTarget', that generates the rectangle used for 'CurrentTarget'.
  • CurrentTarget - This is a rectangle that is generated from the function mentioned above....

Part2 Form1 Load event

Chronologically speaking, this will basically be our first Event, where all the magic starts.... So let's check out what we need to do in Form1's Load event:

I will note a graphics consideration that we should make: We are using GDI with the Form's graphics object in a loop, therefore, in order to reduce flickering, we should double buffer the form. So we set double buffering to true.

Next I have concluded that it is in the best interests of the game to set the forms minimum and maximum size properties. I have decided to make those values identical to each other, being that the form must be the size of the screen, and cannot be any other size. The form also will be started in a maximized state. The location will be the top right of the screen.

Next we generate a random target, for use when the paint event is fired, that way we will have a random target on the screen right away, as soon as the application opens. The way we generate a random target is with the following function

This function returns a random rectangle that is between 100 and 200 pixels tall, and a fixed 100 pixels wide.

This function will set the location of that rectangle: The left position of the rectangle will be no less than the TargetLimitX variable explained earlier.

Finally, we construct our knife, which for this example, consists of manually plotting out the points that make the knife. The way we plot the knife points is with the following function:

The knife polygon is constructed in a clockwise direction starting from the X and Y coordinate.

Notice that each point's coordinate is multiplied by the iScale variable. this allows us to scale the size of our knife poly without having to add dramatic lines of code.

Notice that the code above where it is commented 'Knife Tip' decrements Y when calculating the pt.Y coordinate. This is because it is the upside of the knife. Notice that the code below where the knife tip is commented increments those values instead....

When you connect all these dots, you get a knife polygon! This is true for all these points, all of them that is, except for the "SpecialPoints". What those are are points that you want to be rotated or transformed, whenever the polygon is rotated or transformed, but those points are not a part of the polygon itself. I add a single point that references the location to the center top of the hilt where the center of the blade would meet the hilt. I will need this point later when filling gradients for the top and bottom halves of the knife, but this point is not part of the knife poly itself, and makes it easier to transform it alone, so when we pass the poly to GDI, we don't have to reconstruct the array, removing the special point. I hope that made sense...

Notice that we calculate the origin... This formula is approximately what I 'felt' was the center of mass for that knife... Basically when we rotate this polygon, it will spin on that point.... It wouldn't look right if the entire knife rotated around it's own tip when you threw it....

So now we copy the origin we just made to the OriginalOrigin point(which will never change again in the app!)

Finally now what we have all of our points, we create a new instance of the Polygon Class(explained in detail as we go).

For now I will say that there are 2 constructors for my Polygon Class

Parameter-less, this constructor is used later for cloning the polygon object

That clone is used for validating potential polygon rotations & translations.

This is for preventing the user from positioning the knife poly outside of limits

Constructor with parameters

In order to create a true instance of my polygon class, you need to use the constructor with parameters and feed the following values into the following signature.

  • Sub New(Points As Point(), Origin As Point, SpecialPoints As Point())

We'll dive into what the Polygon class can do in a little while

Finally, Form1's load event is complete!

All of our initialized variables/objects are now standing by.... Waiting for input...

Part 3 - Waiting for input

Somewhere along the line, you may have heard or read that Visual Basic is an Event-Drivin language. What does this mean? This means that unless your application is in the middle of processing an event, then your application is waiting for an event to happen. This is exactly what it is doing when it is done processing your FormLoad event, it begins waiting for events to fire.

What events are we interested in? 

We are mostly interested in two events.

Paint Event

  • This event will be fired when:
  • We invalidate our form
    • Our form's Load Event completes
    • Please note: Mapping out the event chain can get pretty tricky, and usually knowing it is not really necessary, however for more information on this topic, please visit the following link: Order of Events in Windows Forms

User Input

What is user input?

  • Mouse input, Keyboard input, HID device input.

Which devices will we use to get our input?

  • We are only going to handle input from keyboard devices

How will we handle that input?

  • We will handle that input with the following event handler for the Form's keydown event
Private Sub Form1_KeyDown(sender As  Object, e As KeyEventArgs) Handles Me.KeyDown
 Dim sSpeed As Integer  = 15
 Dim oSpeed As Integer  = 10
 Select Case  e.KeyCode
  Case Keys.Up
   Dim TestAngle As Single  = Knife.Angle
   Dim TestOrigin As New Point(Origin.X, Origin.Y)
   TestOrigin.Y -= oSpeed
   If AllowMove(TestOrigin, TestAngle) Then
    Origin.Y -= oSpeed
    Knife.UpdateOrigin(Origin)
   End If
  Case Keys.Down
   Dim TestAngle As Single  = Knife.Angle
   Dim TestOrigin As New Point(Origin.X, Origin.Y)
   TestOrigin.Y += oSpeed
   If AllowMove(TestOrigin, TestAngle) Then
    Origin.Y += oSpeed
    Knife.UpdateOrigin(Origin)
   End If
  Case Keys.Left
   Dim TestAngle As Single  = Knife.Angle
   Dim TestOrigin As New Point(Origin.X, Origin.Y)
   TestOrigin.X -= oSpeed
   If AllowMove(TestOrigin, TestAngle) Then
    Origin.X -= oSpeed
    Knife.UpdateOrigin(Origin)
   End If
  Case Keys.Right
   Dim TestAngle As Single  = Knife.Angle
   Dim TestOrigin As New Point(Origin.X, Origin.Y)
   TestOrigin.X += oSpeed
   If AllowMove(TestOrigin, TestAngle) Then
    Origin.X += oSpeed
    Knife.UpdateOrigin(Origin)
   End If
  Case Keys.W 'up
   Dim TestAngle As Single  = Knife.Angle + sSpeed
   Dim TestOrigin As New Point(Origin.X, Origin.Y)
   If AllowMove(TestOrigin, TestAngle) Then
    Knife.Angle += sSpeed
   End If
  Case Keys.S 'down
   Dim TestAngle As Single  = Knife.Angle - sSpeed
   Dim TestOrigin As New Point(Origin.X, Origin.Y)
   If AllowMove(TestOrigin, TestAngle) Then
    Knife.Angle -= sSpeed
   End If
  Case Keys.Oemplus
   SpinSpeed = CSng(SpinSpeed + 0.5)
   If SpinSpeed > 10 Then SpinSpeed = 5
   Me.Invalidate()
  Case Keys.OemMinus
   SpinSpeed = CSng(SpinSpeed - 0.5)
   If SpinSpeed < 0.5 Then SpinSpeed = 0.5
   Me.Invalidate()
  Case Keys.Q
   Velocity = Velocity - 1
   If Velocity < 1 Then Velocity = 1
   Me.Invalidate()
  Case Keys.E
   Velocity = Velocity + 1
   If Velocity > 15 Then Velocity = 15
   Me.Invalidate()
  Case Keys.H
   Dim Message As New System.Text.StringBuilder
   Message.Append("Knife Thrower Help:" & vbCrLf)
   Message.Append("Press the arrow keys to move the knife" & vbCrLf)
   Message.Append("to where you want to throw it from." & vbCrLf)
   Message.Append(vbCrLf)
   Message.Append("Press 'w' or 's' to rotate the knife before throwing." & vbCrLf)
   Message.Append(vbCrLf)
   Message.Append("Press '+' or '-' to adjust the speed at which the knife spins" & vbCrLf)
   Message.Append("Press 'q' or 'e' to adjust the velocity of the knife" & vbCrLf)
   Message.Append("Press 'h' to open Knife Thrower help menu" & vbCrLf)
   Message.Append("Press 'Space' to throw the knife or to reset the knife after sticking it." & vbCrLf)
   MsgBox(Message.ToString)
  Case Keys.Space
   If Not Thrown Then
    Thrown = True
    If Not Throwing Then
     Throwing = True
     ThrowKnife()
     Throwing = False
    End If
   Else
    If Not Throwing Then
     Thrown = False
     CurrentTarget = RandomTarget()
     ResetKnife()
    End If
   End If
  Case Keys.J
   My.Computer.Audio.Play(My.Resources.knifethrowerjingle, AudioPlayMode.Background)
  Case Keys.Z
   WireFrameOnly = Not  WireFrameOnly
   Me.Invalidate()
  Case Keys.Escape
 End Select
 Knife.Angle = Knife.Angle
 Me.Invalidate()
End Sub

What are sSpeed & oSpeed? 

  • sSpeed is the spin speed for when the user is positioning the knife before a throw
  • oSpeed is the origin speed(velocity) for when the user is positioning the knife before a throw

Please take a moment to notice the select case statement, and that it handles input based on which key on the keyboard was pressed by means of comparing the input key to a constant. 

Please make the following observations:

  • When any arrow key is pressed, the routines that are executed are quite similar, with exception to the following:
    • The increment of TestOrigin.X/TestOrigin.Y is different either by Increment or by .X,.Y
    • The same thing goes for origin
      • This is because if we decrement one of the values in a point(X or Y), this moves the location of the point.
      • So these increments are adjusted depending which key was pressed... 
        • To move a point left, we decrement it's X coordinate
        • To move a point right, we increment it's X coordinate
        • To move a point up, we decrement it's Y coordinate
        • To move a point down, we increment it's Y coordinate
  • I have pointed this out in hopes reducing redundant explanations for this sub.

So when an arrow key is pressed we will first generate some test data.

  • The purpose of this test data is to decide if we will process or ignore that particular input.
    • We would want to ignore it if making the changes resulted in the object being in a prohibited location.
      • Such as the following locations:
        • At an X or Y position of less than 0
        • At an X position greater than our LimitX variable

The test data that we generate will be an identical clone of the knife polygon, we will then apply our transformations/rotations to the clone and test if that clone object is within acceptable regions.

If it is, we allow the transformation/rotation

If it's not, we simply do nothing

The following is the Boolean AllowMove function:

Public Function  AllowMove(TestOrigin As  Point, TestAngle As  Single) As Boolean
 Dim ClonePoly As Polygon = Knife.Clone
 ClonePoly.UpdateOrigin(TestOrigin)
 ClonePoly.Angle = TestAngle
 Dim CandidateRectangle As Rectangle = ClonePoly.ContainerRectangle(0)
 If CandidateRectangle.Right > LimitX Then Return  False
 If CandidateRectangle.Left < 0 Then Return  False
 If CandidateRectangle.Top < 0 Then Return  False
 If CandidateRectangle.Bottom > Me.ClientRectangle.Height Then  Return False
 Return True
End Function

First we create a clone of the knife polygon object.

Next we update the origin of the test object

next we update the angle of the test object

Now we retrieve the ContainerRectangle of the object

  • The zero parameter indicates if we want to use an inside pad in pixels, per side(0=none)

Now we validate all 4 sides of where we want to allow the polygon to move.

This keeps the knife in only the small left section of the screen, while the user is positioning it, getting ready for a throw

Other input keys

W

This rotates the knife before a throw

Dim TestAngle As Single  = Knife.Angle + sSpeed
Dim TestOrigin As New Point(Origin.X, Origin.Y)
If AllowMove(TestOrigin, TestAngle) Then
 Knife.Angle += sSpeed
End If
  • S

    This rotates the knife before a throw

    Dim TestAngle As Single  = Knife.Angle - sSpeed
    Dim TestOrigin As New Point(Origin.X, Origin.Y)
    If AllowMove(TestOrigin, TestAngle) Then
     Knife.Angle -= sSpeed
    End If
    

    + 

    This increases the angle at which the knife spins  per frame during a throwknife routine

    SpinSpeed =  CSng(SpinSpeed + 0.5)
    If SpinSpeed > 10 Then SpinSpeed = 5
    Me.Invalidate()
    

    -

    This decreases the angle at which the knife spins per frame during a throwknife routine

    • SpinSpeed = CSng(SpinSpeed - 0.5)
      If SpinSpeed < 0.5 Then SpinSpeed = 0.5
      Me.Invalidate()
      

    q

    This decreases the velocity at which the origin is incremented per frame during a throwknife routine

    Velocity = Velocity - 1
    If Velocity < 1 Then Velocity = 1
    Me.Invalidate()
    

    e

    This increases the velocity at which the origin is incremented per frame during a throwknife routine

    Velocity = Velocity + 1
    If Velocity > 15 Then Velocity = 15
    Me.Invalidate()
    
  • h

    This will display the help menu

    Dim Message As New System.Text.StringBuilder
    Message.Append("Knife Thrower Help:" & vbCrLf)
    Message.Append("Press the arrow keys to move the knife" & vbCrLf)
    Message.Append("to where you want to throw it from." & vbCrLf)
    Message.Append(vbCrLf)
    Message.Append("Press 'w' or 's' to rotate the knife before throwing." & vbCrLf)
    Message.Append(vbCrLf)
    Message.Append("Press '+' or '-' to adjust the speed at which the knife spins" & vbCrLf)
    Message.Append("Press 'q' or 'e' to adjust the velocity of the knife" & vbCrLf)
    Message.Append("Press 'h' to open Knife Thrower help menu" & vbCrLf)
    Message.Append("Press 'Space' to throw the knife or to reset the knife after sticking it." & vbCrLf)
    MsgBox(Message.ToString)
    
  • j

    This will play the totally awesome Knife Thrower jingle

    • My.Computer.Audio.Play(My.Resources.knifethrowerjingle, AudioPlayMode.Background)
      
  • SpaceBar

    This will throw the knife

    This will reset the knife after you have successfully stuck the knife into a target

    If Not Thrown Then
     Thrown = True
     If Not Throwing Then
      Throwing = True
      ThrowKnife()
      Throwing = False
     End If
    Else
     If Not Throwing Then
      Thrown = False
      CurrentTarget = RandomTarget()
      ResetKnife()
     End If
    End If
    

    Please note in the above code that the following subs/functions are called

    ThrowKnife()

    Public Sub ThrowKnife()
       My.Computer.Audio.Play(My.Resources.flyingKnife3, AudioPlayMode.BackgroundLoop)
     Dim FlewOffScreen As Boolean  = False
     Dim TargetHit As Boolean  = False
     Do
      Origin.X += Velocity
      Knife.UpdateOrigin(Origin)
      Knife.Angle += SpinSpeed
      Me.Invalidate()
      Application.DoEvents()
      TargetHit = RectIntersectsWithPoly(CurrentTarget, Knife.Polygon) 'CurrentTarget.IntersectsWith(Poly1.ContainerRectangle(0))
      FlewOffScreen = Knife.ContainerRectangle(0).Left > Me.ClientRectangle.Width
      If FlewOffScreen Then Exit  Do
      If TargetHit Then Exit  Do
     Loop
     My.Computer.Audio.Stop()
     Select Case  True
      Case FlewOffScreen
       ResetKnife()
       Thrown = False
      Case TargetHit
       If Knife.KnifeDirection = Polygon.Direction.Right Then
        Origin.X += Velocity * 5
        Knife.UpdateOrigin(Origin)
        Knife.Angle += SpinSpeed * 5
        My.Computer.Audio.Play(My.Resources.stick, AudioPlayMode.Background)
       Else
        ResetKnife()
        Thrown = False
       End If
     End Select
    End Sub
    

    FlewOffScreen & TargetHit are used to determine why the knife throw loop was stopped

    • Their name should be self explanatory
    • Based on that reason, an action is taken
    • Please view source code for full details

    RandomTarget()

    • This function returns a random rectangle that will be used as a new 'CurrentTarget'
    • See the code for this function in Part 2 - Form1's Load event

    ResetKnife()

    • This simply moves the knife back to it's original origin

    • Public Sub ResetKnife()
       Origin.X = OriginalOrigin.X
       Origin.Y = OriginalOrigin.Y
       Knife.UpdateOrigin(Origin)
      End Sub
      

     

  • Z

    This will toggle between wireframemode rendering and regular rendering

    WireFrameOnly =  Not WireFrameOnly
    

    Part 4 - The polygon Class

    The following methods are part of the Polygon Class

    The polygon class(Listed completely earlier in the tutorial) is a class that I created for rotating polygons, and there may be more complete versions out there.

    Ok, lets get to this scary thing... Actually it's not too bad...

    Enumerations

    Direction Enum

    • Left
    • Right
    • Vertical
    • PerfcectDiagnol

    Subs/Functions

    • New
      • Used for creating new polygons
    • AdjustPoints
      • Used to move all points when origin has moved
      • Called from UpdateOrigin
    • Clone
      • Used for testing and validating potential object movement/rotation
    • Rad
      • Converts an angle into radians
    • RotatePoint
      • Calculates a point's new location after rotation of n degrees around n origin
    • SetAll
      • Used by the Clone function to set all of the properties  of an instance of Polygon that was created using the parameterless constructor
    • UpdateOrigin
      • Calculates the X,Y difference of the old and new origins, passes those differences to the Adjustpoints sub, which in turn moves all points in the relevant arrays.

    Properties

    • KnifeDirection
      • This is of the Direction Enum type
      • This property determines if the knife is positioned in one of the following directions
        • Left
          • This means that the blade is facing the left(which is a miss)
        • Right
          • This means that the blade is facing the right(which is a hit for us)
        • Vertical
          • This that the blade is either facing up or down(which are both misses)
        • PerfectDiagnol
          • This might produce a valid hit, but for the sake of simplicity, we process this as an invalid hit.
      • This value is determined first comparing the height and width of the knife's containerrectangle
      • Since we know the knife is more long than it is tall, we know that if the rectangle is more wide than it is tall that the knife is horizontal
        • and visa versa for a knife that is vertical
      • If the width and height of that rectangle are the same, we know the knife is sitting in the rectangle diagonally, so we return that
      • Points(0) and points(11) of the knife are at the end of the knife butt, and the tip of the blade. we see which one has a higher X value to determine if the knife is facing left or right. In our case, if 'KnifeDirection' returns 'Right', and if our polygon intersects with the target rectangle, then we know that we have hit a target.
    • ContainerRectangle
      • This property calculates a rectangle by parsing all of the points in the polygon
        • Each of the following will be searched for
          • The smallest Point.X value
          • The smallest Point.Y value
          • The largest Point.X value
          • The largest Point.Y value
        • Based on that information, a rectangle can be generated by the following
          • Left = smallest Point.X Value
          • Top = smallest Point.Y value
          • Width = Largest Point.X value - Smallest Point.X value
          • Height = Largest Point.Y value - Smallest Point.Y value
        • If a padding number is fed to the parameter,
          • Left is decremented by the pad
          • Right is incremented by the pad
          • top is decremented by the pad
          • bottom is incremented by the pad
    • Angle
      • Changing this property will cause all of the points in the Polygon.Polygon & Polygon.tsPoints arrays to be rotated to whatever angle you set this property to.
      • This property is coded in such a way that you could set it to "1000000", and the property automatically would reduce that value to a value that is relevant to 360 degrees(which happens to be 1000000 degrees =280 degrees in this case)

    How to use this class:

    When you are creating a new Polygon from scratch:

    • Wait untill you have calculated all of the following values:
      • Polygon points
      • Special points
      • Polygon origin
    • Once you have calculated all those values, pass then to the New constructor, and you will have created a new instance.

    When you wish to get a copy of your polygon, call the Polygon.Clone method. This is useful for testing.

    When you wish to rotate your polygon, simply set the angle you want it to be rotated to by using the Polygon.Angle Property.

    • Don't forget to invalidate the form you are drawing your polygon to!
    • The polygon has 4 sets of points
      • Polygon.Points
        • This is an array containing the polygon's original untransformed/unrotated points
        • These do update when the origin updates, they do not update when the angle changes
        • This array is mainly preserved for rotations. Rather than calculate off of previous rotations,it is more accurate to use the original points, due to exponential precision loss in single precision numbers. In laymens terms, this prevents your shape from mutating into something else after too many rotations...
      • Polygon.Polygon
        • This is an array containing the Polygon's rotated points, this is the array you should be passing to GDI
      • Polygon.sPoints
        • This array contains your original unrotated Special points, these points do update when the origin is updated, but they do not change when the angle is changed.
      • polygon.tsPoints
        • These points(transformed special points) are kind of like your polygon points. They are calculated from the original sPoints array, each time they are updated... This prevents precision loss. These points are for points that you need to know that arent part of the polygon, but they need to be rotated when the polygon is rotated.

    When you wish to move your polygon up and down, use the Polygon.UpdateOrigin sub

    **Useful Notes about this polygon class

    • The Direction Enum, and knife direction property, is somewhat specific to the knife polygon, but could be removed or reused with non-knife polygons, with little issue.

    Project Source Code

    Download Source Code

    Don't forget to vote 5 stars! :-)

    References

    See Also

    Please view my other Technet Wiki articles

    An important place to find a huge amount of Visual Basic related articles is the TechNet Wiki itself. The best entry point is Visual Basic Resources on the TechNet Wiki

    Hits On Pages

      1. We invalidate our form
      2. Our form's Load Event completes
      • Please note: Mapping out the event chain can get pretty tricky, and usually knowing it is not really necessary, however for more information on this topic, please visit the following link: Order of Events in Windows Forms