📜 ⬆️ ⬇️

A little about the implementation of the puzzle "Soma Cubes" (Swift & SceneKit)

About a year ago, I noticed that Pete Hain’s “Soma Cubes” puzzle, which was invented back in 1933, is not in mobile stores. The desire to try to write a game for iOS was drilled for a long time by an inflamed brain and I finally decided, especially since the design was not particularly needed (drawing a cube in Blender does not count). In the puzzle there are 7 elements from cubes from which other various figures are assembled ( Wikipedia ).

Requirements immediately submitted to the game, boiled down to two points:
1. Do not use third-party frameworks in development.
2. To control the figures and the scene should not be used buttons - only Recognizers.

According to paragraph 2, I mean that the script, when you need to select an object and then click on the control buttons, does not fit.

1. Preparation


We export the cube drawn in Blender to the .dae file and put it in the art.scnassets folder of our game project, during the creation of which the SceneKit was specified as Game Tech. Access to the imported scene and the object on the scene is obtained as follows:
')
let cubeScene = SCNScene(named: "art.scnassets/cube.dae") let cubeNode1 = cubeScene!.rootNode.childNodeWithName("Cube", recursively: false) cubeNode1?.geometry?.materialWithName(CUBE_MATERIAL_NAME)?.diffuse.contents = COLORS_FOR_PRIMITIVES[1] 

The third line simply colors the edges of the cube in the desired color. Now you can clone an object, set coordinates and add it to a parent shape, which is simply an object of class SCNNode. Similarly, place the shape for the assembly in the center of the scene, after making the cube a little smaller.

2. Rotate, raise, lower and drag objects


For the game, it is necessary to provide the ability to rotate the figure 90 degrees around all three axes, which at first caused some difficulties, but then it dawned on me (in a week) that combinations of turns around two axes are enough for all cases. UISwipeGestureRecognizer was chosen for the implementation of the plan. So, a swipe on the figure turns left and right around the vertical axis (Y), regardless of the camera position, the swipe up and down turns the figure either around X or around Z (depending on the camera position).

To drag objects along the XZ plane, it is natural to use the UIPanGestureRecognizer, however, it is necessary to specify the relationship between the “swipe” and the “drag” in order for both handlers to work.

 panGestureRecognizer.requireGestureRecognizerToFail(swipeGestureRecognizer) 

Code for dragging objects for those interested
 func handlePanGestures(recogniser: UIPanGestureRecognizer){ if recogniser.state == .Began { let location = recogniser.locationInView(recogniser.view) let hits = sceneView.hitTest(location,options: nil) as! [SCNHitTestResult] for hit in hits { if Utils.nodeHasPrefix(hit.node.parentNode!, prefix: "fig"){ selectedNode = hit.node.parentNode saveOldPosition(selectedNode) let worldCoord = selectedNode.position let projectedOrigin = sceneView.projectPoint(worldCoord) curZ = projectedOrigin.z let unProj = sceneView.unprojectPoint(SCNVector3Make(Float(location.x), Float(location.y), projectedOrigin.z)) ofset = SCNVector3Make(selectedNode.position.x - unProj.x, selectedNode.position.y - unProj.y, selectedNode.position.z - unProj.z) break } } } if recogniser.state == .Changed { let curScreenPoint = SCNVector3Make(Float(location.x), Float(location.y), curZ) let curWorld = sceneView.unprojectPoint(curScreenPoint) let posPlusOffset = SCNVector3Make(curWorld.x + ofset.x, curWorld.y + ofset.y , curWorld.z + ofset.z) let newPosition = SCNVector3Make(posPlusOffset.x , selectedNode.position.y , posPlusOffset.z ) let projectedOrigin2 = sceneView.projectPoint(newPosition) curZ = projectedOrigin2.z selectedNode.position = newPosition } if recogniser.state == .Ended || recogniser.state == .Failed{ selectedNode.position.x = Utils.clamp(selectedNode.position.x, min : -9 , max: 9) selectedNode.position.y = round(selectedNode.position.y) selectedNode.position.z = Utils.clamp(selectedNode.position.z, min : -9 , max: 9) if testCollision(selectedNode){ selectedNode.position = selectedNodeOldPosition }else{ testGameOver(selectedNode as! SimpleFigure) } selectedNode = nil } } 


For raising and lowering one unit along the Y axis, two instances of UITapGestureRecognizer are used, only one of them has the numberOfTapsRequired = 2, and the envy is also indicated:

 tapRecognizer.requireGestureRecognizerToFail(doubleTapRecognizer) 

3. Camera control


The camera is added to the SCNNode in the center of coordinates at some distance from it and directed in its direction. That is, the camera is located, as it were, on the surface of the sphere, and to rotate the camera, it suffices to rotate the parent SCNNode by a certain angle. For approximation-removal, we use UIPinchGestureRecognizer and it is enough to do the “scale” of the same SCNNode in the handler:

  let cameraNode = SCNNode() cameraNode.name="mainCamera" cameraNode.camera = SCNCamera() cameraNode.camera!.zFar = 100 cameraNode.position = SCNVector3(x: 0.0, y: 0.0, z: 8.0) let cameraOrbit = SCNNode() cameraOrbit.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0) cameraOrbit.addChildNode(cameraNode) cameraOrbit.eulerAngles.x = -Float(M_PI_4) 

I used the eulerAngles property of the cameraOrbit object to increment the angle of rotation in the same handlers as for the figures, previously identifying the object in SCNHitTestResult.

4. Other stuff


To test collisions, the coordinates of each cube of the current figure (which was released, turned, etc.) with the cubes of the other six figures are simply checked. Levels are simply a text file with coordinates.

Following the development of this puzzle, I would like to say that iOS has left a pleasant impression on me, especially the implementation of UIGestureRecognizers. Thank you all for your attention and I hope someone written above will help.

Source: https://habr.com/ru/post/266449/


All Articles