Reputation: 7398
I'm currently working on a game that uses the graphics class to create all the pic's in the game, i'm also trying to paint the pic's from a separate thread to stop my main thread from freezing. But every time i try to run the program, the form appears with nothing on it and i get this error...
System.NullReferenceException: Object reference not set to an instance of an object.
at Single_Player.Form1.mainPaint(PaintEventArgs e) in c:\users\samuel\documents\visual studio 2012\Projects\Single_Player\Single_Player\Form1.vb:line 12
If my code did work i was expecting it to move an ellipse across the screen, anyway here's my code...
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
Dim paintT As New Threading.Thread(AddressOf mainPaint)
paintT.Start()
End Sub
Private Sub mainPaint(e As PaintEventArgs)
e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
e.Graphics.CompositingQuality = Drawing2D.CompositingQuality.HighQuality
Dim playerX As Integer = 0
Dim playerY As Integer = 206
Do While 0 / 0
e.Graphics.DrawRectangle(New Pen(Color.FromArgb(128, Color.Black)), 0, 0, 884, 24)
e.Graphics.FillRectangle(New SolidBrush(Color.FromArgb(128, Color.Black)), 0, 0, 884, 24)
e.Graphics.DrawString("Life: 5", New Font("DigifaceWide", 20, GraphicsUnit.Pixel), New SolidBrush(Color.FromArgb(191, Color.Green)), 2, 0)
e.Graphics.DrawString("Score: 0", New Font("DigifaceWide", 20, GraphicsUnit.Pixel), New SolidBrush(Color.FromArgb(191, Color.White)), 100, 0)
e.Graphics.FillEllipse(New SolidBrush(Color.FromArgb(128, Color.Blue)), playerX, playerY, 24, 24)
playerX = playerX + 1
playerY = playerY + 1
e.Graphics.Clear(Color.Transparent)
Threading.Thread.Sleep(50)
Loop
End Sub
Update (again) My code so far (this time i've added a timer via the design window)...
Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
Timer1.Interval = 50
Timer1.Start()
End Sub
Private Sub mainBackgroundPB_Paint(sender As Object, e As PaintEventArgs) Handles Timer1.Tick
e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
e.Graphics.CompositingQuality = Drawing2D.CompositingQuality.HighQuality
e.Graphics.DrawRectangle(New Pen(Color.FromArgb(128, Color.Black)), 0, 0, 884, 24)
e.Graphics.FillRectangle(New SolidBrush(Color.FromArgb(128, Color.Black)), 0, 0, 884, 24)
e.Graphics.DrawString("Life: 5", New Font("DigifaceWide", 20, GraphicsUnit.Pixel), New SolidBrush(Color.FromArgb(191, Color.Green)), 2, 0)
e.Graphics.DrawString("Score: 0", New Font("DigifaceWide", 20, GraphicsUnit.Pixel), New SolidBrush(Color.FromArgb(191, Color.White)), 100, 0)
e.Graphics.FillEllipse(New SolidBrush(Color.FromArgb(128, Color.Blue)), playerX, playerY, 24, 24)
playerX = playerX + 1
playerY = playerY + 1
e.Graphics.Clear(Color.Transparent)
End Sub
Upvotes: 1
Views: 1977
Reputation:
The answers above are incorrect. It CAN be done, and then it CAN be done even better.
Timers run at a preset interval, which can be good for predictabilitys sake, but bad for performance as it will always run at the same speed or slower. Timers are expensive in games, so use them accordingly, if at all.
Below is a quick example I wrote up showing drawing in a thread, passing it off to a delegate and using a scanning layer to write the image to the main UI.
Suggestions for improvement are commented as well.
Public Class Form1
Enum enumGameObjectTypes
RedBox = 0
BlueBox = 1
YellowCircle = 2
End Enum
Structure structGameObjects
Dim Type As enumGameObjectTypes
Dim Location As Point
End Structure
' When you have global variables, they are accessible outside the thread.
' You cannot access them on the drawing thread however, without using SyncLock to lock
' the object.
Public GameObjects As New List(Of structGameObjects)
Public tDrawingThread As New System.Threading.Thread(AddressOf DrawingThread)
' We have to use delegates to pass the image
Delegate Sub DrawingDelegateSub(TheDrawnImage As Image)
Public DrawingDelegate As DrawingDelegateSub
' Set this to true when it's time to exit cleanly
Public _ExitThreadSafelyNow As Boolean = False
Public Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
Dim rand As New Random
For cnt = 1 To rand.Next(5, 20)
' Create some game objects
Dim newObj As New structGameObjects
With newObj
.Type = rand.Next(0, 2)
.Location = New Point(rand.Next(0, cnt * 100), rand.Next(0, cnt * 50))
End With
Next
' Set up a delegate (basically a worker that can "move data between threads" safely)
DrawingDelegate = AddressOf DrawImage
' Start the thread
tDrawingThread.Start()
End Sub
Public Sub DrawImage(ByVal DrawnImage As Bitmap)
Dim tmpImage As New Bitmap(1024, 768)
' Draw the image to the picture box using an intermediary layer...
' The reason we dont just say pb.Image = DrawnImage is because the bitmaps are passed by pointer references NOT just by the image
' So, passing it direct would be non-thread safe.
'
' This also isn't the *right* way to do this, but it is just an example.
'
' What we're doing is all the heavy lifting in the thread and then we're scanning pixel by pixel through the image drawn by the thread.
'
' The performance here will be awful, so the fix; if you can understand how it works, is to use Bitmap.LockBits;
' Performance will ramp up 1000x right away. https://msdn.microsoft.com/en-us/library/5ey6h79d(v=vs.110).aspx
For x = 1 To DrawnImage.Width - 1
For y = 1 To DrawnImage.Height - 1
tmpImage.SetPixel(x, y, DrawnImage.GetPixel(x, y))
Next
Next
pb.Image = tmpImage
Debug.Print("Wrote frame !")
End Sub
' Main drawing loop
Sub DrawingThread()
Dim rectBounds As New Rectangle(0, 0, 1024, 768)
'
'
Do Until _ExitThreadSafelyNow = True
' Set up the next frame
Dim ImageBuffer As New Bitmap(rectBounds.Width, rectBounds.Height)
Dim g As Graphics = Graphics.FromImage(ImageBuffer) ' Create the graphics object
g.FillRectangle(Brushes.Black, rectBounds)
' Lock the gameobjects object until we're done using it.
SyncLock GameObjects
For Each obj In GameObjects
Select Case obj.Type
Case enumGameObjectTypes.RedBox
g.FillRectangle(Brushes.Red, New Rectangle(obj.Location.X, obj.Location.Y, 15, 15))
'
Case enumGameObjectTypes.BlueBox
g.FillRectangle(Brushes.Blue, New Rectangle(obj.Location.X, obj.Location.Y, 20, 20))
'
Case enumGameObjectTypes.YellowCircle
g.FillEllipse(Brushes.Yellow, New Rectangle(obj.Location.X, obj.Location.Y, 15, 15))
'
Case Else
g.FillEllipse(Brushes.Pink, New Rectangle(obj.Location.X, obj.Location.Y, 15, 15))
End Select
Next
End SyncLock
'/// All done drawing by this point...
'TODO: Eventually implement Bitmap.LockBits here!
SyncLock ImageBuffer
' Send the image onto the main thread.
Call DrawingDelegate(ImageBuffer)
End SyncLock
Loop
End Sub
Private Sub Form1_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing
_ExitThreadSafelyNow = True
End Sub
End Class
Upvotes: 0
Reputation: 941970
You cannot paint in a worker thread, windows are fundamentally thread-unsafe. Your timer Tick event handler will crash and burn when the timer ticks. The Tick event doesn't have a PaintEventArgs argument.
Only the Paint event has those arguments, add that event and move your code. You can trigger the paint with the timer, make the event handler look like this:
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Me.Invalidate()
End Sub
Upvotes: 2