The Mad Engineer Blog
In my last blog entry, I showed how we can use object-oriented programming (OOP) to set up an analysis of a truss. My journey into OOP all started a few weeks ago when my eleven-year-old had to do a project for school and wanted to learn how to code video games. Simple enough, I thought.
A Google search led me to an app called Codea that allows you to code simple 2D video games on an iPad for a mere $15. It comes with some sample games already coded for you. This seemed like a good route for us to take because it was simple and low cost. Codea is in the language of Lua. There isn't much support out there, but I was able to find some useful tutorials on the web. I decided start simple by creating a jazzed up version of 1-player pong.
The idea of Color Pong is that the ball bounces off the left, top, and right sides of the screen, and the player controls a paddle at the bottom. The objective is to keep the ball from crossing the bottom edge of the screen (like the original Pong game). To add some flair to the game, I wanted the ball to change color everytime it hits a surface. I also threw in a sprite of a cat that my five year old helped draw.
From a programming standpoint, the game has two objects: the ball and the paddle, which would give me some experience with OOP. The screen is divided into pixels, and the origin is located at the bottom-left corner of the screen. Global variables WIDTH and HEIGHT give the dimensions of the screen, independent of the resolution on the device. We'll see how to use these in a bit.
The ball is a circle that is characterized by its diameter (or size), its x and y coordinates, and its velocity, which has x and y components. The units on these variables are in pixels. We also attach the score to the ball so that points are added when the ball hits the paddle. We create a class and initialize instances using the following code.
There are several functions related to the ball that we want to define in the class. The first defines the random color assigned to the ball when it hits a surface:
Next we define three functions that describe what to do when the ball hits the left (or west), top (or north), and right (or east) walls. I call these bounceW, bounceN, and bounceE, respectively. For bounceW, for example, we say that when the x-coordinate of the ball minus its radius is less than or equal to zero, the ball bounces.
Note that I am using "if (self.x - self.size/2) <= 0" instead of "if (self.x - self.size) == 0" because the ball travels in frames and the speed can be an odd number that does not neatly divide into WIDTH. I use this approach throughout the code.
Bouncing on the left wall means that the y-component of velocity stays the same but the x-component shifts directions. Thus, self.xSpeed = -self.xSpeed. Upon bouncing, the color changes (i.e., we call on self:color()).
We do something similar for the top and right walls. The code is:
Similarly, we define a function bounceS that takes the position and dimensions of the paddle and determines when ball comes in contact with the paddle. When it does, it changes direction, it changes color, and scores points. The code is as follows:
Lastly, the draw function draws an ellipse with diameter equal to self.size at coordinates self.x and self.y. The ellipse is filled with the random RGB values determined above.
The paddle is defined by its x- and y-coordinates, its width and its thickness. The y-coordinate and the width and thickness of the paddle are fixed. The x-coordinate of the paddle follows the touch measured by the iPad screen. I will explain this below. To create the class and initialize the instance, we use the following:
The only function is the draw function, which draws a rectangle at the position and with the dimensions of the paddle. The paddle is shaded yellow.
The Main Program
In the main program we need to define two functions: setup and draw. The setup function is where we define the initial inputs to the code. In our case, we will initialize the ball and paddle instances by specifying their dimensions, initial coordinates, etc. In the following, we set ball.size = 100, ball.x and ball.y so the ball is at the top-center of the screen, ball.xSpeed and ball.ySpeed to be random numbers between 3 and 8, RGB each to 256 so the ball is white, and ball.score = 0. For the paddle, we position it at the bottom-center, and we give it paddle.width = 250 and paddle.thickness = 20. We also initialize a variable called taps.
The draw function refreshes every frame. Objects are drawn in the order they appear in the draw function. Therefore, we draw the background first; then we place the ball and paddle on the screen; then we draw the sprite on the ball. Doing it in this order means the sprite will be in front of the ball, and the ball and paddle will be in front of the background.
The ball is drawn such that it doesn’t move until the screen is tapped. We do this using the variable CurrentTouch.tapCount, which measures every time the screen is touch. Once touched, then we update the x and y coordinates of the ball based on the assigned speed. To start the ball on touch, we use the following:
We also use the touch screen to control the paddle. We say that the x coordinate of the paddle matches the x coordinate of the touch while the touch is moving on the screen. In code, we have:
Then, we call on the bounce functions for the ball instance and we draw the ball and paddle. For the finishing touch, we draw the cat sprite. The complete code for the main program is as follows:
Putting it all together
Running the code in Codea, we can actually play the game. Below is a sample video of the game in action.
You can see that the game runs smoothly and does everything as it is expected to.
This was a fun little exploration into coding video games. It was a practical application to OOP that allowed me to teach my child some things about coding, like the RGB color palette, x and y coordinates, and the basic structure of an OOP code. My child now wants me to sell the game in the App Store, lol!
I'm a big fan of coding, but I get the sense that my college education may have failed me. Don't get me wrong--I'm proud of my degrees from Pitt and Virginia Tech, and my education was more than enough to propel me into an academic career at the University of Michigan. But the state of the art for computer analysis in civil engineering in the early 2000s was the brute force structure of procedural languages like FORTRAN. Object-oriented programming (OOP) was a topic left to the computer scientists.
Twenty years later, I still follow the old habits that were drilled into my head as an undergrad. However, I've been working more and more in modern languages like Python due to courses I teach on computational analysis. Python is an open source language with countless libraries that can be downloaded seamlessly. It is a powerful tool that can do so much more than the languages from the days of yore. Python's capacity for OOP has always intrigued me, but I wasn't prompted to teach myself OOP until my eleven year old asked me to help program a video game (more on that later).
I'm sharing what I learned so other engineers (especially older folks like me) are encouraged to realize the power behind OOP. To follow along, you need Python 3. I recommend downloading Anaconda for scientific computing in general, and Jupyter Notebook is the quickest way to get started with coding in Python.
In OOP, the properties and functions attributed to an instance are based on classes. Classes are defined in Python according to the following convention:
The first line defines a class with the name "ClassName." Note that the first letter of the class name is capitalized per convention. The function __init__(self, x1, x2, ..., xN) initializes an instance of the class with properties x1, x2, ..., xN. Every instance is characterized by its name ("self") and its properties.
Suppose we want to create a class TrussElements to do linear analysis of trusses. The properties that we associate with a truss element are the x and y coordinates of its two nodes, the cross-sectional area, and Young's modulus. The class definition would be as follows:
Creating Instances of Objects
Once the class is defined, we then create instances of truss elements by calling on the class. Consider the truss in the figure below. Member AB has an area of 2.5 in.^2 and member BC has an area of 1 in.^2. Both members have Young's modulus of 30,000 ksi. A force of 5 k is applied at joint B, and we might be interested in calculating the displacements at the nodes, the reaction forces at the supports, and the axial force in each member of the truss (we won't do all that here, but we will set the problem up).
We create instances of truss elements by calling on TrussElements with the appropriate values for x1, y1, x2, y2, area, and E. Note that we do not need to pass "self" into the class because it is implied by the name we give the instance. For our two bars, AB and BC, we have:
Now that we have created the instances, we can call on their properties. For example, if we want to print the area of element AB, we can do that. If we want to print Young's modulus of element BC, we would do the following:
This returns the values 2.5 and 30000. You can see that it is now convenient for us to call on the instance and its properties using the "." notation.
In addition to ascribing attributes to classes, we can also ascribe methods (or functions) to classes. In the example of the truss, we may want to define a function to compute the length of the truss based on the coordinates of the end nodes. In the class definition, we will add a function Length that does this. Note that we need to import the math library in this case to use the square root function.
Now, calling on the Length function under each instance reports the length of the element:
Here we see the length of AB to be 60.0 and the length of BC to be 36.0. Note that the Length function does not have any arguments because the nodal coordinates are already a part of the class definition.
Adding Output Statements to the Class
So far, we have used print statements outside of the class definition to print output. It may be convenient to format output to be the same for all instances of a class. This can be done using functions, but one particular function that may be of interest is the __str__() function, which modifies what appears when you print(Instance).
Consider our TrussElements class. We add the __str__() function to print as follows:
Now when we print the instance, e.g., BC,
We obtain the following output: "Element goes from (48,36) to (48,0). It has A = 1 and E = 30000"
Arrays of Classes
One final trick is that we can create arrays of classes. In the analysis of trusses, for example, we often loop over the elements in the structure and use an index to indicate the element under consideration. From the previous example, suppose we number AB as element 1 and BC as element 2. Joint A is node 1, joint B is node 2, and joint C is node 3. We will store coordinate information in a "coords" array.
To use arrays, we must call upon the numpy library.
Since we will loop over all elements (nels = 2), it is also convenient to store the areas in an array. We can also use a "connectivity" array to store the inode and jnode for each element. The connectivity array is as follows:
In Python, the input looks as follows:
We initialize the truss array and enter a loop over the elements. For each element, we extract the nodal coordinates, area, and Young's modulus, and enter them into the TrussElement class definition. We append the information for element i to the truss array.
We can then print whatever output we need. For example, the following prints the length, area, and E for each element.
In this blog, we studied basic concepts of classes, instances, methods, and arrays as they apply to OOP. We looked at how these concepts can be applied to structural analysis by considering how data is stored for computational analysis of trusses. We have just scratched the surface of OOP, but hopefully you are inspired to learn more.