Created
December 29, 2019 01:46
-
-
Save niaeashes/2005b0292bd7aee6af53727c7fae3893 to your computer and use it in GitHub Desktop.
ゲーム作る.Hex座標系.log
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class HexCell { | |
let coordinate: HexCoordinate | |
private(set) var field: Field = .none | |
private var isFreezed: Bool = false | |
init(coordinate: HexCoordinate) { | |
self.coordinate = coordinate | |
} | |
convenience init(q: Int, r: Int) { | |
self.init(coordinate: HexCoordinate(q: q, r: r)) | |
} | |
func freeze() { | |
isFreezed = true | |
} | |
func setField(_ field: Field) { | |
guard isFreezed == false else { | |
assertionFailure() | |
return | |
} | |
self.field = field | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private let HEX_SIZE: Double = 3 | |
private let HEX_WIDTH: Double = sqrt(3) * HEX_SIZE | |
private let HEX_HEIGHT: Double = 2 * HEX_SIZE | |
private let HEX_HORIZONTAL_SPACE: Double = HEX_WIDTH | |
private let HEX_VERTICAL_SPACE: Double = HEX_HEIGHT * 3.0 / 4.0 | |
private let HEX_CENTER_DISTANCE: Double = HEX_WIDTH | |
private let ε: Double = 0.001 | |
private let ANGLE_REAL_DEGREE: Double = 60 | |
private let WALL_SIZE: Double = HEX_SIZE | |
/// Cross product of Vector. | |
infix operator ** | |
private typealias Vector = SIMD2<Double> | |
private typealias Point = SIMD2<Double> | |
extension SIMD2 where Scalar == Double { | |
fileprivate init(from s: Point, to t: Point) { | |
self = t - s | |
} | |
fileprivate init(from c: HexCoordinate) { | |
let x = c.qd * HEX_HORIZONTAL_SPACE + c.rd * HEX_HORIZONTAL_SPACE / 2.0 | |
let y = HEX_VERTICAL_SPACE * -c.rd | |
self.init(x, y) | |
} | |
fileprivate static func **(lhs: SIMD2<Scalar>, rhs: SIMD2<Scalar>) -> Scalar { | |
return lhs.x * rhs.y - lhs.y * rhs.x | |
} | |
fileprivate func roundHexCoordinate() -> HexCoordinate { | |
let r = Darwin.round(-y / HEX_VERTICAL_SPACE) | |
let q = Darwin.round((x - r * HEX_HORIZONTAL_SPACE / 2.0) / HEX_HORIZONTAL_SPACE) | |
return HexCoordinate(q: Int(q), r: Int(r)) | |
} | |
fileprivate var vectorLength: Scalar { return sqrt(pow(x, 2) + pow(y, 2)) } | |
} | |
private struct Segment { | |
let s: Point | |
let v: Vector | |
init(_ s: Point, _ t: Point) { | |
self.s = s | |
self.v = Vector(from: s, to: t) | |
} | |
// return true, when segment is crossing. | |
static func ^(lhs: Segment, rhs: Segment) -> Bool { | |
let v = Vector(from: lhs.s, to: rhs.s) | |
let t1 = (v ** lhs.v) / (lhs.v ** rhs.v) | |
let t2 = (v ** rhs.v) / (lhs.v ** rhs.v) | |
return 0.0 <= t1 && t1 <= 1.0 && 0.0 <= t2 && t2 <= 1.0 | |
} | |
} | |
extension HexWallBlock { | |
func isInterrupting(from sourceCoordinate: HexCoordinate, to targetCoordinate: HexCoordinate) -> Bool { | |
let distance = sourceCoordinate.hexDistance(from: targetCoordinate) | |
let sReal = Point(from: sourceCoordinate) | |
let tReal = Point(from: targetCoordinate) | |
let diff = Vector(from: sReal, to: tReal) | |
let count = distance | |
let targets = Set((0...count).map { ( sReal + ( diff / Double(count) * Double($0) ) ).roundHexCoordinate() }) | |
.filter { self.contains($0) } | |
if targets.count == 0 { | |
return false | |
} | |
return targets.contains { target in | |
let cReal = Point(from: target) | |
let vector = Vector(from: cReal, to: sReal) | |
// conflict the center circle of wall. | |
if (vector.vectorLength * sin(atan2(vector.x, vector.y))).magnitude < WALL_SIZE / 2 { | |
return true | |
} | |
let arounds = target.aroundCoordinates.filter { self.contains($0) } | |
return arounds.contains { around in | |
let s1 = Segment(sReal, tReal) | |
let s2 = Segment(cReal, Point(from: around)) | |
return s1 ^ s2 // return true, when segment is crossing. | |
} | |
} | |
} | |
} | |
extension HexCoordinate { | |
func hexDistance(from another: HexCoordinate) -> Int { | |
return (abs(q - another.q) + abs(q + r - another.q - another.r) + abs(r - another.r)) / 2 | |
} | |
func isNearBy(_ target: HexCoordinate, hexRange range: Int) -> Bool { | |
return hexDistance(from: target) <= range | |
} | |
private func radian(to target: HexCoordinate) -> Double { | |
let sReal = Point(from: self) | |
let tReal = Point(from: target) | |
return atan2(tReal.y - sReal.y, tReal.x - sReal.x) | |
} | |
fileprivate var qd: Double { Double(q) } | |
fileprivate var rd: Double { Double(r) } | |
func isInSight(_ sight: HexSpaceSight, source: HexCoordinate) -> Bool { | |
if source == self { | |
return true | |
} | |
if sight.angle <= 0 { // no sight. | |
return false | |
} | |
if sight.angle >= 6 { // 360° over viewing angle. | |
return true | |
} | |
let realRadian = source.radian(to: self) | |
let directionRadian = HexCoordinate.zero.radian(to: HexCoordinate(sight.direction)) | |
let viewingAngleRadian = (Double(sight.angle) * ANGLE_REAL_DEGREE / 360.0 * 2.0 * .pi) // angle is Int, 1 angle equals 60° | |
let lhs = directionRadian - viewingAngleRadian / 2.0 | |
let rhs = directionRadian + viewingAngleRadian / 2.0 | |
if lhs <= realRadian + ε, realRadian - ε <= rhs { | |
return true | |
} | |
if .pi * 2.0 < rhs { // Really necessary? | |
let test = rhs - .pi * 2.0 | |
if 0.0 <= realRadian, realRadian <= test + ε { | |
return true | |
} | |
} | |
if .pi * -2.0 > lhs { // Really necessary? | |
let test = lhs + .pi * 2.0 | |
if test - ε <= realRadian, realRadian <= 0.0 { | |
return true | |
} | |
} | |
return false | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
typealias HexPath = Array<HexCoordinate> | |
typealias HexWallBlock = Set<HexCoordinate> | |
extension HexWallBlock { | |
func isTouched(by coordinate: HexCoordinate) -> Bool { | |
return contains { $0.isTouching(to: coordinate) } | |
} | |
} | |
class HexSpace { | |
let cells: Array<HexCell> | |
var objects: Array<HexSpaceObject> = [] | |
var positions: Dictionary<Int, HexCoordinate> = [:] | |
var nextHexId = 1 | |
init(_ cells: Array<HexCell>) { | |
cells.forEach { $0.freeze() } | |
self.cells = cells | |
} | |
convenience init(use factory: HexSpaceFactory) { | |
self.init(factory.build()) | |
} | |
func checkCellExists(at coordinate: HexCoordinate) -> Bool { | |
return cells.contains { $0.coordinate == coordinate } | |
} | |
func coordinate(of object: HexSpaceObject) -> HexCoordinate { | |
return positions[object.hexId]! | |
} | |
func findObjects(in sight: HexSpaceSight, from object: HexSpaceObject) -> Array<HexSpaceObject> { | |
return objects | |
.filter { self.coordinate(of: $0).isVisible(in: sight, source: self.coordinate(of: object)) } | |
} | |
func findPaths(maxRange: Int, from sourceCoordinate: HexCoordinate, to targetCoordinate: HexCoordinate) -> Array<HexPath>? { | |
guard checkCellExists(at: sourceCoordinate) else { return nil } | |
if sourceCoordinate.hexDistance(from: targetCoordinate) > maxRange { | |
return nil | |
} | |
var fringes: Array<Array<HexCoordinate>> = [[sourceCoordinate]] | |
var visited: Set<HexCoordinate> = [] | |
var step = 0 | |
while visited.contains(targetCoordinate) == false && step <= maxRange { | |
defer { step += 1 } | |
fringes.append([]) | |
fringes[step].forEach { coordinate in | |
coordinate.aroundCoordinates | |
.filter { self.checkCellExists(at: $0) } | |
.forEach { coordinate in | |
visited.insert(coordinate) | |
fringes[step + 1].append(coordinate) | |
} | |
} | |
} | |
guard visited.contains(targetCoordinate) else { return nil } | |
let lastStep = step | |
do { | |
var paths: Array<HexPath> = [[targetCoordinate]] | |
var step = lastStep - 1 | |
while step >= 0 { | |
defer { step -= 1 } | |
let range = 0..<paths.count | |
for index in range { | |
let path = paths[index] | |
let nextCoordinates = fringes[step].filter { $0.isTouching(to: path.last!) } | |
paths[index] = path + [nextCoordinates.first!] | |
if nextCoordinates.count > 1 { | |
for i in 1..<nextCoordinates.count { | |
paths.append(path + [nextCoordinates[i]]) | |
} | |
} | |
} | |
} | |
return paths.map { $0.reversed() } | |
} | |
} | |
func findWallBlocks() -> Array<HexWallBlock> { | |
let walls = cells | |
.filter { $0.field.isWall } | |
.map { $0.coordinate } | |
var results: Array<HexWallBlock> = [] | |
var usedList: Set<HexCoordinate> = [] | |
let use: (HexCoordinate) -> Void = { usedList.insert($0) } | |
let used: (HexCoordinate) -> Bool = { usedList.contains($0) } | |
while usedList.count < walls.count { | |
guard let target = walls.filter({ !used($0) }).first else { break } | |
use(target) | |
var wallBlock: HexWallBlock = [target] | |
while let target = walls.first(where: { wallBlock.isTouched(by: $0) && !used($0) }) { | |
wallBlock.insert(target) | |
use(target) | |
} | |
results.append(wallBlock) | |
} | |
return results | |
} | |
} | |
extension HexCoordinate { | |
func isVisible(in sight: HexSpaceSight, source: HexCoordinate) -> Bool { | |
if self.isNearBy(source, hexRange: sight.hexRange) == false { | |
return false // target is out of viewing range. | |
} | |
if self.isInSight(sight, source: source) == false { | |
return false // target is not in viewing angle. | |
} | |
return true | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class HexSpaceFactory { | |
typealias Builder = (HexCell) -> Void | |
var builders: Array<(HexCoordinate, Builder?)> = [] | |
func set(at coordinate: HexCoordinate, builder: Builder? = nil) { | |
if let index = builders.firstIndex(where: { $0.0.q == coordinate.q && $0.0.r == coordinate.r }) { | |
builders[index] = (coordinate, builder) | |
} else { | |
builders.append((coordinate, builder)) | |
} | |
} | |
func addLine(start: HexCoordinate, direction: HexDirection, length: Int, builder: Builder? = nil) { | |
(0..<length).forEach { | |
self.set(at: start + (direction.vector * $0), builder: builder) | |
} | |
} | |
func addTriangle(point: HexCoordinate, direction: HexDirection, length: Int, rotation: HexRotation = .clockwise, builder: Builder? = nil) { | |
var l = length | |
var s = point | |
let ad = direction.rotate(rotation) | |
while (l > 0) { | |
defer { | |
l -= 1 | |
s = s + HexCoordinate(ad) | |
} | |
addLine(start: s, direction: direction, length: l, builder: builder) | |
} | |
} | |
func addHexagram(center: HexCoordinate, range: Int, builder: Builder? = nil) { | |
(0...5).forEach { i in // [!] Each HexDirection values. | |
let d = HexDirection(rawValue: i)! | |
self.addTriangle(point: center, direction: d, length: range, builder: builder) | |
} | |
} | |
func build() -> Array<HexCell> { | |
return builders.map { args in | |
let (coordinate, builder) = args | |
let cell = HexCell(coordinate: coordinate) | |
builder?(cell) | |
return cell | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
protocol HexSpaceObject: AnyObject { | |
// Unique ID on some hex space. | |
var hexId: Int { get set } | |
// The token exclusively holds a cell, another tokens can't pass and stop holded cells. | |
var isToken: Bool { get } | |
} | |
extension HexSpaceObject { | |
var isOrnament: Bool { !isToken } | |
} | |
// MARK: - HexSpace Methods for managing HexSpaceObject. | |
extension HexSpace { | |
@discardableResult | |
func place(_ object: HexSpaceObject, at coordinate: HexCoordinate) -> Bool { | |
guard contains(object) == false else { | |
assertionFailure() | |
return false | |
} | |
defer { nextHexId += 1 } | |
assert(object.hexId <= 0) | |
object.hexId = nextHexId | |
objects.append(object) | |
if checkCellExists(at: coordinate) == false { | |
assertionFailure() | |
return false | |
} | |
if checkTokenExists(at: coordinate) == true { | |
assertionFailure() | |
return false | |
} | |
positions[object.hexId] = coordinate | |
return true | |
} | |
func remove(_ object: HexSpaceObject) { | |
guard let index = index(of: object) else { | |
assertionFailure() | |
return | |
} | |
defer { object.hexId = 0 } | |
if let index = positions.index(forKey: object.hexId) { | |
positions.remove(at: index) | |
} | |
objects.remove(at: index) | |
} | |
func checkTokenExists(at coordinate: HexCoordinate) -> Bool { | |
return objects | |
.filter { $0.isToken } | |
.map { positions[$0.hexId] } | |
.contains(coordinate) | |
} | |
func contains(_ object: HexSpaceObject) -> Bool { | |
return objects.contains { $0 === object } | |
} | |
private func index(of object: HexSpaceObject) -> Array<HexSpaceObject>.Index? { | |
return objects.firstIndex { $0 === object } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum HexDirection: Int, Hashable, Equatable { | |
case right = 0 | |
case upRight = 1 | |
case upLeft = 2 | |
case left = 3 | |
case downLeft = 4 | |
case downRight = 5 | |
func rotate(_ rotation: HexRotation) -> HexDirection { | |
switch rotation { | |
case .clockwise: | |
return HexDirection(rawValue: (rawValue + 5) % 6)! | |
case .anticlockwise: | |
return HexDirection(rawValue: (rawValue + 1) % 6)! | |
} | |
} | |
} | |
enum HexRotation { | |
case clockwise | |
case anticlockwise | |
} | |
struct HexCoordinate: CustomDebugStringConvertible, Hashable, Equatable { | |
var q: Int | |
var r: Int | |
init(q: Int, r: Int) { | |
self.q = q | |
self.r = r | |
} | |
init(_ direction: HexDirection, range: Int = 1) { | |
self = HexCoordinate.unitVectors[direction]! * range | |
} | |
var debugDescription: String { | |
return "[q: \(q), r: \(r)]" | |
} | |
var aroundCoordinates: Array<HexCoordinate> { | |
return HexCoordinate.unitVectors.values.map { self + $0 } | |
} | |
func hash(into hasher: inout Hasher) { | |
hasher.combine("\(q),\(r)") | |
} | |
func isTouching(to: HexCoordinate) -> Bool { | |
return self.hexDistance(from: to) <= 1 | |
} | |
static func +(lhs: HexCoordinate, rhs: HexCoordinate) -> HexCoordinate { | |
return HexCoordinate(q: lhs.q + rhs.q, r: lhs.r + rhs.r) | |
} | |
static func *(lhs: HexCoordinate, rhs: Int) -> HexCoordinate { | |
return HexCoordinate(q: lhs.q * rhs, r: lhs.r * rhs) | |
} | |
static func ==(lhs: HexCoordinate, rhs: HexCoordinate) -> Bool { | |
return lhs.q == rhs.q && lhs.r == rhs.r | |
} | |
static let zero: HexCoordinate = HexCoordinate(q: 0, r: 0) | |
static let unitVectors: Dictionary<HexDirection, HexCoordinate> = [ | |
.right: HexCoordinate(q: 1, r: 0), | |
.upRight: HexCoordinate(q: 1, r: -1), | |
.upLeft: HexCoordinate(q: 0, r: -1), | |
.left: HexCoordinate(q: -1, r: 0), | |
.downLeft: HexCoordinate(q: -1, r: 1), | |
.downRight: HexCoordinate(q: 0, r: 1), | |
] | |
} | |
struct Field { | |
let isWall: Bool | |
let floorHeight: Int | |
static let none = Field(isWall: false, floorHeight: 0) | |
} | |
struct HexSpaceSight { | |
var direction: HexDirection | |
var angle: Int // 0 ~ 6, means viewing angle. (calc angle * 60°) | |
var hexRange: Int | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://note.com/niaeashes/n/n1ae0ca91c837