After investing in some tools, a few months of working evenings, and a ten day wait in the App Store approval queue my second iOS app is now available.
Initial Thoughts
The Easter Bunny was kind enough to wait in line on release day at my local Apple store and deliver to wife a shiny new iPad. When I saw the resolution and clarity of the screen I knew I wanted to do a game for it. After looking for an hour at the simple games that already existed in the app store, it occurred to me that–since some of the apps had set the bar pretty low–it shouldn’t be too hard to improve on a graphical 3D Tic Tac Toe. And since 3x3x3 Tic Tac Toe is no challenge what-so-ever, I decided to go to 4x4x4–hence the name of the game Four³.
Four³ is a true 3D, four-in-a-row implementation of Tic Tac Toe.
Tools
My initial thinking was to do the game in OpenGL (which led to my previous post) but as I researched what it would take to implement my game it became apparent that using OpenGL would require me to do more than just the graphics and I was really hoping to get something into the App Store sooner rather than later. Since I didn’t want to give up all my family time, and my App Store exposure is not (yet) great enough for me to quit my day job, I decided to invest in some game development tools to simplify the process.
I googled 3D game engines and wound up selecting Unity3D. After downloading the demo (and a gracious extension of said demo after a two week halt in development due to the uncertainty of the new iOS 4 TOS) I was able to almost fully prototype my application with only the demo license. At that point I was convinced the $300 cost of the Unity iPhone basic license was warranted. Although games based on the Unity engine are still being approved in the App Store the folks at Unity3D are working hard on a workaround to reduce the ambiguity about the new terms of service and the use of a tool like Unity.
The Game Board
My 4x4x4 tic tic toe game board is simply defined by 9 intersecting planes which delineate 64 game spaces. These planes are easily fabricated.
The game tokens were also easy to generate–the “O” token is simply a sphere and the “X” token is six carefully arranged smaller cubes combined into a single “prefab”.
So the game board and the player tokens were the easy part; more complicated was determining how to allow players to select a game space. In my prototyping phase I realized it would be a simply matter to allow the player to choose a smaller sphere–a “move dot”– which is pre-populated in a game space. The Unity engine allows the kinematics to be defined on an object-by-object basis, so it was a simple matter to configure the grid planes not to respond to touches and configure the “move dots” to do so. Essentially the translucent planes defining the board are invisible to touches. (This is the type of thing that would have been much more time consuming had I gone straight to OpenGL.)
Suspending the game board in space was a simple matter of surrounding it with a skybox.
Since the game tokens can quickly fill any single plain of the game board, it was imperative that the user be able to rotate/spin the board to be able to view the unused game spaces.
The Game Play
It was easy to determine the set of winning vectors. It was a simple matter to track when either player had control of winning vector–control being defined as one player, but not both, having a token in the vector. It was easy to implement the two-player game as no AI was needed, other than determining when a draw had occurred. Implementing the AI for the more advanced device game play slowed me down a bit. I actually set things aside for about ten days to fiddle with some other development.
I decided to make the easy level really easy–on this level the device simply picks a random unused game space. This makes it very easy to beat the device as there is really no offensive or defensive strategy involved when the device chooses its next move.
The medium and hard levels present different combinations of offensive and defensive strategy. I will not be revealing the full details, but I will say that much of the decision making is based on how much control a given player has of a given win vector and the hard level presents a greater defensive strategy than the medium level. I was quite pleasantly surprised when, even as the developer, the hard level beat me the first two out of three games I played against it (I let the device go first.)
iPhone Input Sample Script
The primary purpose of this post is to return something to the Unity community. I learned an awful lot from the forums and other resources I discovered.
Below is a portion of my game script which deals with iPhone touch input. There was no single example available when I started my research that showed quite this much interaction so I am publishing this to help others with their Unity development.
The highlights in this script include:
- Detecting taps to select a move dot
- Detecting a swipe to rotate the game board
- Detecting pinches to zoom in and out
I hope someone finds it useful.
Example.js
// // Control all user interactions here // #pragma strict private var touchBegan: boolean; private var previous: Vector2; private var swipe: int; private var dx: float; private var dy: float; private var dVec: Vector3; private var minDist: float; private var maxDist: float; private var moveFactor: float; private var minMajorDist: float; private var curDist: Vector2; private var prevDist: Vector2; private var touch2: iPhoneTouch; private var nTouch; private var dpos: Vector2; private var pinch: boolean; private var slide: float; private var nextDeviceMove: GameObject; private var go: GameObject; private var pos: Vector3; private var hit: RaycastHit; private var currentPlayer: int; private var undoLimit: float; private var undoAllowed: boolean; private var undo: boolean; private var devicePlays: int; private var timeSinceLastMove: float; private var iPhoneInUse: boolean; private var myPosition: Vector2; private var rotationRate: float; static var touch: iPhoneTouch; static var popup : boolean; static var tap: boolean; function Start() { resetGamePlay(); // currentPlayer and devicePlays set by settings screen Debug.Log("PlayGame() - currentPlayer ("+currentPlayer+")"); if (devicePlays && 2 == currentPlayer) { DeviceMove(); } } function resetGamePlay() { touchBegan = false; swipe = 0; pinch = false; dVec = Vector3.zero; minDist = 16; maxDist = 30; moveFactor = 0.05; minMajorDist = 15; maxMinorDist = 7; popup = false; tap = false; defRotationRate = 45.0; rotationRate = defRotationRate; undoLimit = 2.0; nextDeviceMove = null; orientationReset = iPhoneSettings.screenOrientation; undoAllowed = false; undo = false; transform.LookAt(Vector3.zero); iPhoneInUse = (iPhoneSettings.model.Substring(0,1) == "i"); } // // Check for iPhone Touches here // function FixedUpdate () { // // if the popup menu is visible don't do normal processing // if (popup) return; timeSinceLastMove += Time.deltaTime; if (undoAllowed && timeSinceLastMove > undoLimit) { undoAllowed = false; Debug.Log("Time for device move "+timeSinceLastMove); if (devicePlays && 2 == currentPlayer) { DeviceMove(); } } if (!iPhoneInUse) { // get position from mouse (for developemnt only) tap = Input.GetMouseButtonUp(0); myPosition = Input.mousePosition; return; } // // Decode touches here // nTouch = iPhoneInput.touchCount; if (nTouch == 1) { pinch = false; touch = iPhoneInput.GetTouch(0); if (touch.phase == iPhoneTouchPhase.Began) { dvec = Vector3.zero; previous = touch.position; touchBegan = true; swipe = 0; tap = false; } else if (touchBegan && touch.phase == iPhoneTouchPhase.Moved) { dpos = touch.position - previous; dx = Mathf.Abs(dpos.x); dy = Mathf.Abs(dpos.y); if (dx >= minMajorDist && dy <= dx) { // swipe in x-axis swipe = (dpos.x<0) ? -1 : 1; previous = touch.position; dVec = Vector3.up; } else if (dy >= minMajorDist && dx <= dy) { // swipe in y-axis swipe = (dpos.y<0) ? -1 : 1; previous = touch.position; dVec = -transform.right; } } else if (touch.phase == iPhoneTouchPhase.Ended) { touchBegan = false; tap = (0 == swipe); swipe = 0; } } else if (nTouch == 2) { pinch = false; touch = iPhoneInput.GetTouch(0); touch2 = iPhoneInput.GetTouch(1); dVec = Vector3.zero; if (touch.phase == iPhoneTouchPhase.Moved && touch2.phase == iPhoneTouchPhase.Moved) { curDist = touch.position - touch2.position; prevDist = (touch.position - touch.deltaPosition) - (touch2.position - touch2.deltaPosition); slide = moveFactor * (prevDist.magnitude - curDist.magnitude); mag = transform.position.magnitude; slide = Mathf.Clamp(mag + slide, minDist, maxDist); dVec = Vector3.forward * (mag - slide); pinch = true; } } } function Update () { // // process all frame updates here // // show next device move if (null != nextDeviceMove) { PlayerMove(nextDeviceMove.transform.position); Destroy(nextDeviceMove); nextDeviceMove = null; } if (undo) { undo = false; LastMoveUndo(); } if (popup) return; // // Do 3D dtuff here // if (swipe != 0) { // rotate around the origin along the selected major axis (dVec) transform.RotateAround (Vector3.zero, dVec, swipe * rotationRate * Time.deltaTime); //swipe = swipe - k*i; // As coded we get a continuos rotation if the swipe has not ended, // even when the touch is held stationary. // // Uncomment the line below to stop rotation when touch is // stationary but not ended // swipe = 0; } else if (pinch) { // move the camera in and out based on how far we pinched transform.Translate(dVec); // make sure we're still looking at the origin transform.LookAt(Vector3.zero); // don't pinch on next update pinch = false; } // // Check to see if the user selected a MoveDot // // don't process taps while we're in the undo time interval if (!tap || undoAllowed) return; tap = false; if (iPhoneInUse) { myPosition = touch.position; } // We need to actually tap on an object if (!Physics.Raycast(Camera.main.ScreenPointToRay(myPosition), hit, 100)) return; // And we need to hit a rigidbody that is not kinematic if (!hit.rigidbody || hit.rigidbody.isKinematic) return; go = hit.rigidbody.gameObject; // get position of move dot that was tapped pos = go.transform.position; // destroy move dot that was tapped Destroy(go); undoAllowed = PlayerMove(pos); } function PlayerMove(pos: Vector3) { // place player token in gameboard return true; } function DeviceMove() { // logic for next device move // nextDeviceMove = Game Object of selected move dot } function LastMoveUndo() { // remove player token // restore move dot undo = false; }
3 replies on “A New App — Four³”
Hi there,
It’s good for someone like you to share your ideas in unity as shown in Four³. I really appreciate that especially there is the whole example javascript shown there.
However, I have encountered some problems when trying to apply your sample script at my end. I am trying to zoom the main camera in and out within a specify distance BUT all I can do is just zoom the camera in and cannot zoom it out. AND, it seems that the line “slide = Mathf.Clamp(mag + slide, minDist, maxDist);” is used to adjust the speed of the zooming and not the distance constraints.
Here is my code:
[CODE]
private var pinch:boolean;
static var touch:Touch;
private var touch2:Touch;
private var curDist:Vector2;
private var prevDist:Vector2;
private var actualDist:float;
private var moveFactor:float;
private var mag:float;
private var maxDist:float;
private var minDist:float;
private var zoomDist:Vector3;
var target:Transform;
function Start() {
resetGamePlay();
}
function resetGamePlay() {
pinch = false;
zoomDist = Vector3.zero;
minDist = 0;
maxDist = 10;
moveFactor = 0.05;
transform.LookAt(target);
}
function FixedUpdate(){
if(Input.touchCount == 2){
pinch = false;
touch = Input.GetTouch(0);
touch2 = Input.GetTouch(1);
zoomDist = Vector3.zero;
if(touch.phase == TouchPhase.Moved && touch2.phase == TouchPhase.Moved){
curDist = touch.position – touch2.position;
prevDist = (touch.position – touch.deltaPosition) – (touch2.position – touch2.deltaPosition);
actualDist = moveFactor * (prevDist.magnitude – curDist.magnitude);
mag = transform.position.magnitude;
actualDist = Mathf.Clamp(mag + actualDist, minDist, maxDist);
zoomDist = Vector3.forward * actualDist;
pinch = true;
}
}
}
function Update(){
if(pinch){
transform.Translate (zoomDist);
transform.LookAt(target);
pinch = false;
}
}
[/CODE]
I am looking forward to your reply and really appreciate it if you can show me a correct way. Thank you in advance.
Best regards,
Victor Yew
Your interpretation of the clamp() function was wrong. I used this to limit the position of the camera in order to keep the user from zooming in to far, in which case they’d be inside the game cube, or too far out in which case the the game cube would be too small. Both cases would result in the user being unable to play the game.
To be honest, I am about a year removed from this code and I currently do not have the time to refresh my Unity scripting skills. Unfortunately, I really don’t have much to offer in the way of advice.
I did notice, however, your minDist is zero. I always had a min distance greater than zero.
Good luck with your project.
Thanks for the reply. I do notice your intention of using the Clamp function to limit the zooming and that’s what i want too! However, the zooming goes as far as the target.position and it stucks there.
The main problem now is I can zoom in BUT not zooming out. Whenever I open the fingers, the camera zooms in perfectly, but when I close the fingers, it zooms in as well! It just doesn’t zoom out. I’ve got the feeling I am almost there.
Is there anything to do with the Translate function? I tried insert a “-” in front of the zoomDist, it still stays the same. Thanks for your help.
P.S. I did play around with the minDist and maxDist value, it seems that they just affect the speed of the zooming and doesn’t limit the position of the camera.